Skip to main content

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. PUT creates a formation, or replaces an existing one with the same name in the same scope. There is no POST.
  • Imports are asynchronous. PUT validates the whole request, then returns 202 Accepted with a jobId. 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. PUT sets a formation's surfaces and color. enabled, maxDepth and maxThickness are set with PATCH.
  • 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:

FieldDescription
latitudeWGS84 latitude, -90 to 90
longitudeWGS84 longitude, -180 to 180
topTvdTrue vertical depth of the formation's top, in feet, positive down
baseTvdTrue 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:

scopeWhere 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/formationsProject: /v1/projects/{projectId}/formations
List and readEvery 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 formationsWrites this project's formations
Update (PATCH)Any formation in the company, by idOnly this project's formations
DeleteCompany formations onlyOnly this project's formations
JobsEvery import started through the APIImports 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.

FieldTypeRequiredDescription
namestringYesUp to 100 characters. Leading and trailing spaces are trimmed. If a formation with this name exists in the scope, it is replaced.
pointsarrayYes3 to 50,000 points. See What a formation is.
colorstringNoHex 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:

ValueEffect
trim (default)The base is raised to the top of the formation below, so the two don't overlap.
keepBoth 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:

ChangesKept
All of its points (top and base)Its id, so references to it keep working
color, if you send oneenabled, 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": []
}
}
statusMeaning
"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 fieldDescription
formationsEach formation in the request, with its number of points and whether it had a base
written.createdNew formations
written.replacedExisting formations whose data changed
written.adjustedOther formations in the scope, not in the request, whose placeholder bases were raised onto a formation in this import. See How Bases Are Resolved.
overlapsPairs 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.
skippedRowsPoints the import skipped
rowErrorsWhy 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:

  1. Wait for the import job to succeed.
  2. Find the formation's id: GET /v1/projects/{projectId}/formations?name=Formation%20A.
  3. 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 baseTvd on 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:

FilterDescription
scopecompany or project. project without projectId returns every project formation.
projectIdOnly this project's formations. Can't be combined with scope=company.
nameExact 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. take defaults to 10,000, which is also the maximum.
  • Follow the next link in the Link header until it is gone. X-Query-Count is 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:

  1. GET /v1/formations/{id} for the summary.
  2. Read every page of GET /v1/formations/{id}/points.
  3. 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.

note

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.

FieldDescription
nameUp to 100 characters. Must be unique in the formation's scope, or that record fails with 409.
colorHex color such as #c0392b.
enabledWhether the formation is shown in the gun barrel plot.
maxDepthTVD in feet that the formation's base is limited to. null removes the limit.
maxThicknessThickness 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.

caution

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 PUT instead, 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​

RuleDetail
Formations per request1 to 200
Points per formation3 to 50,000
Points per request200,000 in total
NamesRequired, 1 to 100 characters after trimming, unique within the request
CoordinatesLatitude -90 to 90, longitude -180 to 180
Duplicate pointsNo two points in a formation can have the same latitude and longitude
topTvdRequired, a finite number
baseTvdOn every point of a formation or on none. Where given, deeper (greater) than topTvd.
color# followed by 6 hex digits
Scope fieldsscope and projectId, if sent, must match the route
Unknown fieldsRejected

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 StatusCondition
400Validation error. details.location points to each problem. Nothing is imported.
400scope or projectId doesn't match the route
404Formation, 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