Formations API Reference
The Formations API manages the geologic formation surfaces used by the ComboCurve map and gun barrel plot. You can import formations from your own data, read them back point by point, change their display settings, and delete them.
These endpoints work differently from most of the API:
- Formations are written by name, not by id.
PUTcreates a formation, or replaces an existing one with the same name in the same scope. There is noPOST. - Imports are asynchronous.
PUTvalidates the whole request, then returns202 Acceptedwith ajobId. You poll the job for the outcome. - An import can change formations that weren't in the request. Formations in a scope are treated as one stack, so importing a formation can adjust the formation above it. See How Bases Are Resolved.
- Display settings are set separately.
PUTsets a formation's surfaces and color.enabled,maxDepthandmaxThicknessare set withPATCH. - Points are read from their own endpoint. Summaries don't include points. Each formation's points are paginated at
/formations/{id}/points.
Key Concepts
What a formation is
A formation is a named surface defined by points. Each point has a location and a top depth, and optionally a base depth:
| Field | Description |
|---|---|
latitude | WGS84 latitude, -90 to 90 |
longitude | WGS84 longitude, -180 to 180 |
topTvd | True vertical depth of the formation's top, in feet, positive down |
baseTvd | True vertical depth of the formation's base, in feet, positive down. Optional. |
Scope
Every formation belongs either to the company or to exactly one project:
scope | Where it is used |
|---|---|
"company" | Company formations, available in every project |
"project" | One project's formations. The summary's projectId says which project. |
A formation's name is unique within its scope. Company formations and each project's formations are separate stacks: importing into a project never reads or changes company formations, and the reverse.
A formation can't be moved to a different scope. To move one, read it, PUT it into the new scope, and delete the original.
Top-level and project routes
Top level: /v1/formations | Project: /v1/projects/{projectId}/formations | |
|---|---|---|
| List and read | Every formation in the company: company formations and every project's. Filter with scope, projectId and name. | Only this project's formations |
Import (PUT) | Writes company formations | Writes this project's formations |
Update (PATCH) | Any formation in the company, by id | Only this project's formations |
| Delete | Company formations only | Only this project's formations |
| Jobs | Every import started through the API | Imports into this project |
Workflow: Import Formations
1. PUT /v1/formations?overlapHandling=trim → 202 { jobId }
2. GET /v1/formations/jobs/{jobId} → poll until successful
3. (optional) GET /v1/formations?name=... → find the ids
PATCH /v1/formations → set enabled, maxDepth, maxThickness
Step 1: Send the formations
PUT /v1/projects/66aa1b2c3d4e5f6a7b8c9d0e/formations?overlapHandling=trim
Content-Type: application/json
[
{
"name": "Formation A",
"color": "#c0392b",
"points": [
{ "latitude": 35.0, "longitude": -100.1, "topTvd": 8000.0, "baseTvd": 8300.0 },
{ "latitude": 35.0, "longitude": -100.0, "topTvd": 8010.0, "baseTvd": 8310.0 },
{ "latitude": 35.1, "longitude": -100.05, "topTvd": 8005.0, "baseTvd": 8305.0 }
]
},
{
"name": "Formation B",
"points": [
{ "latitude": 35.0, "longitude": -100.1, "topTvd": 8300.0 },
{ "latitude": 35.0, "longitude": -100.0, "topTvd": 8310.0 },
{ "latitude": 35.1, "longitude": -100.05, "topTvd": 8305.0 }
]
}
]
The body is an array of formations. A single formation object is also accepted.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Up to 100 characters. Leading and trailing spaces are trimmed. If a formation with this name exists in the scope, it is replaced. |
points | array | Yes | 3 to 50,000 points. See What a formation is. |
color | string | No | Hex color such as #c0392b. When omitted, a new formation gets a default color and a replaced one keeps its color. |
Query parameter overlapHandling (optional) controls what happens where a formation's base is deeper than the top of the formation below it:
| Value | Effect |
|---|---|
trim (default) | The base is raised to the top of the formation below, so the two don't overlap. |
keep | Both depths are stored as given. |
It applies to the whole request and only matters for formations that include baseTvd.
202 Accepted
{ "jobId": "6703f2a0b1c2d3e4f5a6b7c8" }
The whole request is validated before anything is imported. If anything is wrong, the API returns 400 with the location of each problem, such as [1].points[17].baseTvd, and imports nothing. See Validation Rules.
What replacing a formation changes
When a formation with the same name already exists in the scope:
| Changes | Kept |
|---|---|
| All of its points (top and base) | Its id, so references to it keep working |
color, if you send one | enabled, maxDepth and maxThickness |
Formations in the scope that aren't named in the request are kept. You don't need to resend the whole stack to update one formation.
Step 2: Poll the job
GET /v1/formations/jobs/6703f2a0b1c2d3e4f5a6b7c8
A project import's job can be read on that project's /jobs/{jobId} route or on the top-level route.
200 OK
{
"jobId": "6703f2a0b1c2d3e4f5a6b7c8",
"status": "successful",
"createdAt": "2026-10-09T14:01:00.000Z",
"completedAt": "2026-10-09T14:01:40.000Z",
"result": {
"formations": [
{ "name": "Formation A", "points": 3, "hasBase": true },
{ "name": "Formation B", "points": 3, "hasBase": false }
],
"written": { "created": 1, "replaced": 1, "adjusted": 0 },
"overlaps": [],
"skippedRows": 0,
"rowErrors": []
}
}
status | Meaning |
|---|---|
"pending" | Queued and not started yet |
"running" | In progress |
"successful" | Done. result describes what was written. |
"failed" | The import failed. error has the reason. |
Polling every 5 to 10 seconds is reasonable. An import that hasn't finished after 3 hours is marked failed with the error Import timed out. Please retry the import.
| Result field | Description |
|---|---|
formations | Each formation in the request, with its number of points and whether it had a base |
written.created | New formations |
written.replaced | Existing formations whose data changed |
written.adjusted | Other formations in the scope, not in the request, whose placeholder bases were raised onto a formation in this import. See How Bases Are Resolved. |
overlaps | Pairs of formations where the upper one's base is deeper than the lower one's top: above, below, how many points conflict out of how many were compared, and the largest overlap in feet. With trim, these overlaps were trimmed. With keep, they were stored as given. |
skippedRows | Points the import skipped |
rowErrors | Why points were skipped, with their location in your request, such as [0].points[17]. Rare, because the request is validated first. |
Step 3 (optional): Set display settings
PUT doesn't set enabled, maxDepth or maxThickness. A new formation starts with enabled: true and no depth limits. To change them:
- Wait for the import job to succeed.
- Find the formation's id:
GET /v1/projects/{projectId}/formations?name=Formation%20A. - Send a
PATCH.
You only need to do this once per formation. Later imports of the same name keep these settings.
How Bases Are Resolved
The formations in a scope form a stack, ordered by depth. Where a formation has no baseTvd, its base is taken from the top of the formation below it.
- Give
baseTvdon every point of a formation, or on none. A formation with a base on only some points is rejected. - The deepest formation without a base has nothing below it. It gets a placeholder base of 50,000 ft.
- Importing a formation below a formation with a placeholder base raises that placeholder onto the new formation's top. This changes a formation that wasn't in your request, and is counted in
written.adjusted. - Company formations and each project's formations are separate stacks. A project's formations are never adjusted by company formations, or the reverse.
When you read points back, a placeholder base is returned as "baseTvd": 50000 with "baseIsPlaceholder": true. Send 50000 back unchanged and it stays a placeholder, so it can still be raised by a later import. PUT ignores baseIsPlaceholder.
Reading Formations
Summaries
GET /v1/formations returns a page of formation summaries, and GET /v1/formations/{id} returns one. Summaries don't include points.
[
{
"id": "6703f9a1b2c3d4e5f6a7b8c9",
"name": "Formation A",
"color": "#c0392b",
"enabled": true,
"scope": "project",
"projectId": "66aa1b2c3d4e5f6a7b8c9d0e",
"maxDepth": null,
"maxThickness": null,
"bbox": [-100.1, 35.0, -100.0, 35.1],
"pointCount": 110,
"createdAt": "2026-10-09T14:02:11.000Z",
"updatedAt": "2026-10-09T14:02:11.000Z"
}
]
The list takes skip and take (default 25, at most 200), with paging links in the Link header. At the top level you can filter by:
| Filter | Description |
|---|---|
scope | company or project. project without projectId returns every project formation. |
projectId | Only this project's formations. Can't be combined with scope=company. |
name | Exact name. |
HEAD /v1/formations (or GET /v1/formations/head, for clients that can't send HEAD) returns the number of matching formations in X-Query-Count.
Points
GET /v1/formations/6703f9a1b2c3d4e5f6a7b8c9/points?skip=0&take=10000
[
{ "latitude": 35.0, "longitude": -100.1, "topTvd": 8000.0, "baseTvd": 8300.0 },
{ "latitude": 35.2, "longitude": -100.2, "topTvd": 9000.0, "baseTvd": 50000, "baseIsPlaceholder": true }
]
- Points are returned in stored order.
takedefaults to 10,000, which is also the maximum. - Follow the
nextlink in theLinkheader until it is gone.X-Query-Countis the formation's total number of points. - Every point has a
baseTvd: the base you sent, the top of the formation below, or the 50,000 ft placeholder.
Reading a formation in a form you can send back
Reads are designed to round-trip. To get a formation in the exact shape PUT accepts:
GET /v1/formations/{id}for the summary.- Read every page of
GET /v1/formations/{id}/points. - Add the collected points to the summary as
points.
The result can be sent to PUT unchanged, on the same route. The read-only fields (id, enabled, scope, projectId, maxDepth, maxThickness, bbox, pointCount, createdAt, updatedAt) are accepted and ignored, as is baseIsPlaceholder on each point. A scope or projectId that doesn't match the route is rejected with 400, so a formation read from one project can't be sent to another by mistake; remove those two fields to copy a formation into a different scope.
If a formation is re-imported while you are paging through its points, you could get a mix of old and new points. To be sure you didn't, read GET /v1/formations/{id} again after the last page and check that updatedAt and pointCount haven't changed.
Formations imported in the ComboCurve app have no point limit. One with more than 50,000 points can be read through the API but not sent back with PUT.
Updating Formations
PATCH changes a formation's name and display settings without re-importing it.
PATCH /v1/projects/66aa1b2c3d4e5f6a7b8c9d0e/formations
Content-Type: application/json
[
{ "id": "6703f9a1b2c3d4e5f6a7b8c9", "maxDepth": 11500, "maxThickness": null, "enabled": true }
]
207 Multi-Status
{
"results": [{ "status": "Success", "code": 200, "id": "6703f9a1b2c3d4e5f6a7b8c9" }],
"successCount": 1,
"failedCount": 0
}
The body is an array of up to 200 records, each with an id. A single object is also accepted. Each record gets its own result.
| Field | Description |
|---|---|
name | Up to 100 characters. Must be unique in the formation's scope, or that record fails with 409. |
color | Hex color such as #c0392b. |
enabled | Whether the formation is shown in the gun barrel plot. |
maxDepth | TVD in feet that the formation's base is limited to. null removes the limit. |
maxThickness | Thickness in feet that the formation is limited to. null removes the limit. |
Only the fields you send are changed. A record must change at least one of them. Read-only fields from a GET are accepted and ignored, except that scope and projectId must match the formation's own.
On the top-level route you can patch any formation in the company. On a project route, a formation outside the project fails with 404 in its result.
Imports match formations by name. If you rename Formation A to Formation A1 and later PUT a formation named Formation A, the import creates a new formation rather than updating the renamed one.
Deleting a Formation
DELETE /v1/projects/66aa1b2c3d4e5f6a7b8c9d0e/formations/6703f9a1b2c3d4e5f6a7b8c9
Returns 204 No Content once the formation is deleted. Deletion is immediate; there is no job.
DELETE /v1/formations/{id}deletes company formations only. Delete a project's formation on its project route.- A formation that doesn't exist, or isn't in the route's scope, returns
404. - Deleting doesn't cascade. Inventory plan landing zones that use the formation keep a reference to it. To update a formation's surfaces without breaking those references, re-import it with
PUTinstead, which keeps its id. - Formations above it are not adjusted. A formation whose base was taken from the deleted one keeps that base.
Validation Rules
| Rule | Detail |
|---|---|
| Formations per request | 1 to 200 |
| Points per formation | 3 to 50,000 |
| Points per request | 200,000 in total |
| Names | Required, 1 to 100 characters after trimming, unique within the request |
| Coordinates | Latitude -90 to 90, longitude -180 to 180 |
| Duplicate points | No two points in a formation can have the same latitude and longitude |
topTvd | Required, a finite number |
baseTvd | On every point of a formation or on none. Where given, deeper (greater) than topTvd. |
color | # followed by 6 hex digits |
| Scope fields | scope and projectId, if sent, must match the route |
| Unknown fields | Rejected |
Errors use locations such as [2].name, [0].points[17].baseTvd or overlapHandling. When one formation has many bad points, reporting stops after the first batch so the response stays readable; fix those and resubmit.
Error Reference
| HTTP Status | Condition |
|---|---|
400 | Validation error. details.location points to each problem. Nothing is imported. |
400 | scope or projectId doesn't match the route |
404 | Formation, job or project not found, or the formation isn't in the route's scope |
409 | (PATCH, per record) The new name is already used in the formation's scope |