Skip to main content

Shapefiles (Map Layers) API Reference

The Shapefiles API manages the custom map layers shown on the ComboCurve map, such as DSU outlines, lease boundaries or pipeline routes. You can create a layer, read its features back, share it with more projects, and delete it, all without the ComboCurve app.

These endpoints work differently from most of the API:

  • Creating a layer is asynchronous. A create request returns 202 Accepted with a jobId. You poll the job until the layer is ready.
  • Zip files are uploaded straight to cloud storage. The API gives you a signed URL. You PUT the zip to that URL, then ask the API to import it. The file never passes through the API itself.
  • Names must be unique where the layer appears. Creating a layer with a name that is already in use returns 409 Conflict. There is no replace-in-place: to replace a layer, delete it and create it again.
  • One layer can belong to several projects. Deleting it from a project route only removes it from that project if other projects still use it.

Key Concepts​

Scope​

Every layer has a scope:

scopeWhere the layer appears
"company"On the map in every project
"project"Only in the projects listed in its projectIds (one or more)

projectIds always lists every project the layer belongs to, even when you read the layer through a single project's route. Check it before deleting a layer to see who else uses it.

Top-level and project routes​

Each operation is available on two sets of routes:

Top level: /v1/shapefilesProject: /v1/projects/{projectId}/shapefiles
List and readEvery layer in the company: company layers and every project's. Filter with scope, projectId and name.Only the layers in this project. Company layers are not included.
CreateBody sets scope, plus projectIds when the scope is projectAlways creates a layer in this project. The body has no scope or projectIds.
Update (PATCH)Name, description, color, scope and projectsNot available
DeleteDeletes the layer everywhereRemoves the layer from this project only (see Deleting a Layer)
JobsAny upload or delete job you startedOnly jobs that involved this project

Use the top-level routes to share a layer across projects or to manage company layers. Use project routes when an integration only ever works inside one project.

Jobs​

Creating a layer, and deleting one, run in the background. The request returns 202 Accepted:

{ "jobId": "6703f2a0b1c2d3e4f5a6b7c8" }

Poll GET /v1/shapefiles/jobs/{jobId} (or the project route's /jobs/{jobId}) until status is successful or failed. See Polling a Job.


Choosing How to Create a Layer​

There are two ways to create a layer. Both produce the same kind of layer, and both read back the same way.

Zip uploadGeoJSON
Best forShapefiles you already have, large layers, data in a projected coordinate systemData your integration generates, or layers read from another system
Calls3: reserve an upload, upload the file, import it1
Coordinate systemAny, as long as the zip includes a .prj file. Layers are reprojected to WGS84 on import.WGS84 longitude/latitude only
Size limit260 MB zip10,000 features, and a 24 MB request body
ValidationThe zip's file list is checked up front. Problems with the shapes themselves fail the job.Every feature is checked up front. Errors return 400 with the location of each problem.

Workflow A: Upload a Shapefile Zip​

1. POST /v1/shapefiles/uploads               → uploadId and a signed uploadUrl
2. PUT <uploadUrl> → the zip goes to cloud storage
3. POST /v1/shapefiles/uploads/{uploadId} → 202 { jobId }
4. GET /v1/shapefiles/jobs/{jobId} → poll until successful

What the zip must contain​

  • Exactly one shapefile: a .shp file with a matching .shx and .dbf file (the same base name).
  • A .prj file is optional but recommended. Without one, coordinates are read as WGS84 longitude/latitude.
  • Files can sit at the root of the zip or in a folder. macOS metadata (__MACOSX/, ._* files) is ignored.
  • At most 260 MB.

Step 1: Reserve an upload​

POST /v1/shapefiles/uploads
Content-Type: application/json

{ "contentType": "application/zip" }

The body is optional. contentType can be application/zip (the default) or application/x-zip-compressed, which some Windows tools send.

201 Created

{
"uploadId": "6703f1c2a9e4b5d6e7f80912",
"uploadUrl": "https://storage.googleapis.com/...",
"method": "PUT",
"headers": { "Content-Type": "application/zip" },
"expiresAt": "2026-10-12T15:30:00.000Z",
"maxBytes": 272629760
}
FieldDescription
uploadIdIdentifies this upload. You use it in step 3.
uploadUrlSigned URL to upload the zip to. It expires at expiresAt, 30 minutes after it was issued.
methodAlways PUT.
headersHeaders the upload request must send, exactly as given.
maxBytesThe largest zip that can be imported (260 MB).

An upload is tied to the route that reserved it. An upload reserved on /v1/shapefiles/uploads must be imported on /v1/shapefiles/uploads/{uploadId}, and one reserved on a project route must be imported on that same project route.

Step 2: Upload the zip​

Send the file's bytes to uploadUrl with the method and headers from step 1:

curl -X PUT "<uploadUrl>" \
-H "Content-Type: application/zip" \
--data-binary @dsu_outlines.zip
note

This request goes to Google Cloud Storage, not to the ComboCurve API. Send only the headers from step 1, and do not send your ComboCurve API key or token. If the Content-Type doesn't match the one in step 1, storage rejects the upload.

If the URL expires before the upload finishes, reserve a new upload and start again.

Step 3: Import the upload​

The body holds the layer's details. The file is already in storage.

POST /v1/shapefiles/uploads/6703f1c2a9e4b5d6e7f80912
Content-Type: application/json

{
"name": "DSU Outlines",
"description": "Q3 DSUs",
"color": "#a0a0a0",
"scope": "project",
"projectIds": ["66aa1b2c3d4e5f6a7b8c9d0e", "66bb1b2c3d4e5f6a7b8c9d0f"]
}
FieldTypeRequiredDescription
namestringYesSee Layer Names.
descriptionstringNoFree text.
colorstringNoHex color such as #a0a0a0 (the default).
scopestringYes, top level only"company" or "project".
projectIdsstring[]When scope is "project"One or more existing project ids. Must be omitted when scope is "company".

On a project route, leave out scope and projectIds. The layer is created in that project.

202 Accepted

{ "jobId": "6703f1c2a9e4b5d6e7f80912" }

Before starting the job, the API checks that:

  1. The upload exists and is no larger than 260 MB. Otherwise it returns 404, or 400 if the file is too large.
  2. The zip contains one complete shapefile. Otherwise it returns 400 with the reason, such as a missing .dbf.
  3. The name is available. Otherwise it returns 409 (see Layer Names).

Import an upload within 24 hours of uploading it. Uploads that are never imported are deleted after that.

Each upload can be imported once. Repeating the import returns 409 with the original job's id in details.jobId. If your import request timed out and you don't know whether it went through, retry it: either it starts the job, or the 409 tells you which job to follow.

Always poll the jobId from the response (or from details.jobId). Don't assume it equals the uploadId.


Workflow B: Create a Layer from GeoJSON​

Send the layer's details and a GeoJSON FeatureCollection in one request:

POST /v1/shapefiles
Content-Type: application/json

{
"name": "DSU Outlines",
"description": "Q3 DSUs",
"color": "#a0a0a0",
"scope": "company",
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [[[-100.10, 35.00], [-100.00, 35.00], [-100.00, 35.10], [-100.10, 35.10], [-100.10, 35.00]]]
},
"properties": { "NAME": "DSU 1", "ACRES": 1280 }
}
]
}

202 Accepted

{ "jobId": "6703f2a0b1c2d3e4f5a6b7c8" }

name, description, color, scope and projectIds follow the same rules as a zip import. On a project route, leave out scope and projectIds.

GeoJSON rules​

The layer is stored as a shapefile, so shapefile limits apply on top of the GeoJSON format:

RuleDetail
CoordinatesWGS84 [longitude, latitude]. Longitude between -180 and 180, latitude between -90 and 90.
Features1 to 10,000, and a request body under 24 MB. Upload larger layers as a zip.
One kind of geometryEvery feature must be points (Point, MultiPoint), lines (LineString, MultiLineString) or polygons (Polygon, MultiPolygon). You can mix single and multi geometries of the same kind.
PolygonsEach ring is closed (first and last positions are equal) and has at least 4 positions.
LinesAt least 2 positions.
Property names1 to 10 letters, digits or _. Names that differ only by case, such as Name and NAME, are rejected. Names are never truncated, so rename long ones before sending.
Property valuesA string, number, boolean or null. No nested objects or arrays.
StringsAt most 254 bytes (UTF-8).
NumbersFinite, smaller than 1015, with at most 15 decimal places.
One type per propertyA property that is a number in one feature and a string in another is rejected. null is allowed anywhere.

Validation errors return 400 with a location for each problem, such as features[12].geometry.coordinates[0] or features[3].properties.LEASE_NAME. Up to 50 errors are reported at once.

Features keep the order you send them in. Feature ids are not used.


Polling a Job​

GET /v1/shapefiles/jobs/6703f2a0b1c2d3e4f5a6b7c8

200 OK

{
"jobId": "6703f2a0b1c2d3e4f5a6b7c8",
"type": "upload",
"status": "successful",
"createdAt": "2026-10-12T15:01:00.000Z",
"completedAt": "2026-10-12T15:01:40.000Z",
"result": { "shapefileId": "6703e9a1b2c3d4e5f6a7b8c9" }
}
FieldDescription
type"upload" (a create from either workflow) or "delete".
statusSee below.
completedAtPresent once the job has finished.
result.shapefileIdThe new layer's id, on a successful upload.
errorThe reason, when status is "failed".
statusMeaning
"pending"Queued and not started yet.
"running"In progress. An upload reports running from the moment it is accepted until it finishes. No progress percentage is available.
"successful"Done. For an upload, the layer is ready and result.shapefileId is its id.
"failed"The job failed. See error.

Uploads usually take from a few seconds to a few minutes, depending on the size of the layer. Polling every 5 to 10 seconds is reasonable.

An upload that hasn't finished after 30 minutes is marked failed with the error Processing timed out. Please retry the upload. To retry a zip upload, reserve a new upload and repeat the workflow. To retry a GeoJSON create, send the request again.

Only jobs started through the API can be read here; uploads made in the ComboCurve app are not available. A project route only returns jobs that involved its project.


Layer Names​

Format​

A name is 2 to 64 characters: letters, digits, _, - and spaces, with no leading or trailing space. Wells is reserved.

Where a name must be unique​

A name must not already be used anywhere the new layer will appear:

New layerMust not match
Company layerAny company layer
Project layer in projects P1…PnAny company layer, or any layer in P1…Pn

The comparison is exact and case-sensitive: DSU Outlines and dsu outlines are different names. As in the ComboCurve app, a new company layer is only checked against other company layers, so it can take a name that a project layer already uses.

A conflict returns 409 Conflict. The body tells you what's in the way:

{
"name": "ShapefileConflictError",
"message": "A layer named `DSU Outlines` already exists in this scope.",
"details": { "location": "name", "conflictingId": "6703e9a1b2c3d4e5f6a7b8c9" }
}
details containsCauseWhat to do
conflictingIdAn existing layer has the nameChoose another name, or delete that layer and try again (see Replacing a Layer)
jobIdAnother upload with the name is still in progressWait for that job to finish, or choose another name
NeitherA heatmap layer has the name. Heatmaps can't be read or deleted through the API.Rename or delete the heatmap in ComboCurve, or choose another name

Replacing a Layer​

Layers can't be replaced in place. To upload a new version under the same name:

  1. Find the existing layer, for example with GET /v1/shapefiles?name=DSU%20Outlines.
  2. Delete it with DELETE /v1/shapefiles/{id}. This returns 202 with a delete jobId.
  3. Poll the delete job until status is successful. The name is in use until then, so creating the new layer earlier returns 409.
  4. Create the new layer with either workflow.
caution

The new layer has a new id. As in the ComboCurve app, deleting and re-creating a layer loses what referred to the old one: users' map settings (whether the layer is switched on, and its display settings) don't carry over, and inventory plans that use the old layer for DSUs keep pointing at the deleted layer.

To change only the name, description, color or projects, use PATCH instead. It keeps the layer's id.


Sharing and Updating a Layer​

PATCH /v1/shapefiles changes a layer's details without re-uploading it. It is only available on the top-level route.

PATCH /v1/shapefiles
Content-Type: application/json

[
{ "id": "6703e9a1b2c3d4e5f6a7b8c9", "projectIds": ["66aa1b2c3d4e5f6a7b8c9d0e", "66bb1b2c3d4e5f6a7b8c9d0f", "66cc1b2c3d4e5f6a7b8c9d10"] }
]

207 Multi-Status

{
"results": [{ "status": "Success", "code": 200, "id": "6703e9a1b2c3d4e5f6a7b8c9" }],
"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, so some can succeed while others fail. Send each layer at most once per request.

FieldDescription
nameRenames the layer. The new name gets the same uniqueness check as a new layer, against every project the layer will belong to.
descriptionnull clears it.
colorHex color.
scope"company" or "project". Moving to "company" clears projectIds, so don't send both.
projectIdsThe complete new list of projects. It replaces the old list rather than adding to it.

Only the fields you send are changed. shapeType, bbox and fields are accepted and ignored, so you can send back a record from GET.

Common changes:

ToSend
Share a project layer with another projectThe full projectIds, with the new project added
Remove a layer from one of its projectsThe full projectIds, without that project. A project layer must keep at least one project; use DELETE to remove it completely.
Make a project layer a company layer"scope": "company"
Make a company layer a project layer"scope": "project" and projectIds

Deleting a Layer​

What DELETE does depends on the route:

RouteLayerResult
DELETE /v1/shapefiles/{id}Any layerThe layer is deleted everywhere. 202 with a delete jobId.
DELETE /v1/projects/{projectId}/shapefiles/{id}Belongs only to this projectThe layer is deleted. 202 with a delete jobId.
Also belongs to other projectsThe layer is removed from this project only and stays in the others. 200, immediately.
A company layer, or not in this project404

When a shared layer is removed from a project, the response lists the projects that still have it:

{
"id": "6703e9a1b2c3d4e5f6a7b8c9",
"result": "unlinked",
"remainingProjectIds": ["66bb1b2c3d4e5f6a7b8c9d0f"]
}

The layer's name becomes available in that project straight away. After a 202, the name stays in use until the delete job succeeds.

Deleting a layer removes it from users' map settings. Inventory plans that use the layer for DSUs keep a reference to it.


Reading Layers​

List and get​

GET /v1/shapefiles returns a page of layers. GET /v1/shapefiles/{id} returns one layer.

[
{
"id": "6703e9a1b2c3d4e5f6a7b8c9",
"name": "DSU Outlines",
"description": "Q3 DSUs",
"color": "#a0a0a0",
"scope": "project",
"projectIds": ["66aa1b2c3d4e5f6a7b8c9d0e", "66bb1b2c3d4e5f6a7b8c9d0f"],
"shapeType": "POLYGON",
"bbox": [-100.10, 35.00, -100.00, 35.10],
"fields": [{ "name": "NAME", "fieldType": "string" }]
}
]
FieldDescription
shapeTypePOINT, POLYLINE, POLYGON or MULTIPOINT.
bbox[minLongitude, minLatitude, maxLongitude, maxLatitude].
fieldsThe layer's attribute columns.

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 layer.
projectIdOnly layers in this project. Can't be combined with scope=company.
nameExact name.

HEAD /v1/shapefiles (or GET /v1/shapefiles/head, for clients that can't send HEAD) returns the number of matching layers in X-Query-Count.

Heatmap layers are not included in any of these.

Features​

A layer's shapes come from a separate, paginated endpoint:

GET /v1/shapefiles/6703e9a1b2c3d4e5f6a7b8c9/features?skip=0&take=200
[
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [[[-100.10, 35.00], [-100.00, 35.00], [-100.00, 35.10], [-100.10, 35.10], [-100.10, 35.00]]]
},
"properties": { "NAME": "DSU 1", "ACRES": 1280 }
}
]
  • The response is an array of GeoJSON Feature objects in WGS84, in their original order.
  • take defaults to 200, which is also the maximum. Follow the next link in the Link header until it is gone.
  • X-Query-Count is the layer's number of features.
  • A layer uploaded as a zip reads back as GeoJSON, reprojected to WGS84. The original zip can't be downloaded.

Copying a layer​

What the API returns can be sent back to create a layer. To copy a layer, for example to another tenant or into a new layer with a different name:

  1. GET /v1/shapefiles/{id} for its name, description and color.
  2. Read every page of GET /v1/shapefiles/{id}/features.
  3. POST /v1/shapefiles with those details, a scope (and projectIds), "type": "FeatureCollection", and the collected features as features.

This works for layers of up to 10,000 features. Copy larger layers by uploading the original zip.


Limits​

LimitValue
Zip size260 MB
Signed upload URL lifetime30 minutes
Time to import an upload24 hours after uploading
GeoJSON features per layer10,000
Request body24 MB
Upload job timeout30 minutes, after which the job is marked failed
Records per PATCH200
Layers per page (GET)25 by default, 200 at most
Features per page200

Error Reference​

HTTP StatusCondition
400Invalid request body, such as a bad name, color or GeoJSON feature. details.location points to the problem.
400The upload is not a zip, doesn't contain exactly one complete shapefile, or is larger than 260 MB
400PATCH would leave a project layer with no projects, or sends projectIds with scope: "company"
404Layer, job or project not found, or the layer isn't in the route's project
404Upload not found: not uploaded yet, older than 24 hours, or reserved on a different route
409The name is already in use where the layer would appear (see Layer Names)
409The upload was already imported. details.jobId is the original job.
409The uploaded file changed while it was being checked. Retry the import.
502The upload could not be started. Retry the request.