Skip to main content

Bulk Create Geofences from Addresses

The bulk-create API is the integration entry point for syncing address-based geofences into SpatialFlow from your existing systems. Each address is geocoded and turned into a circular geofence in a single call — no GeoJSON required.

When to Use This

Use this endpoint when you need to:

  • Sync from a CRM or TMS — import customer sites, service zones, or delivery stops from Salesforce, HubSpot, Onfleet, or any flat address list
  • Onboard a new customer — turn a customer's address list into geofences in one API call
  • Run a scheduled refresh — re-import a CSV nightly; dedup_strategy: "skip" makes re-imports idempotent (duplicates return a match report, not an error)
  • One-shot CSV import — a team member pastes addresses into a script and fires a single POST to get all fences created

For interactive imports with a preview step, use the /api/v1/geofences/preview endpoint before committing.

Authentication

API keys are recommended for integrations. API key calls get a 60 requests/hour limit per workspace (6× the JWT limit), and they don't expire between user sessions.

Generate an API key from Dashboard → Account → API Keys, then include it in the X-API-Key header:

curl https://api.spatialflow.io/api/v1/geofences/bulk \
-H "X-API-Key: sf_live_..." \
-H "Content-Type: application/json" \
-d '{"items": [{"address": "123 Main St, San Francisco, CA"}]}'

JWT tokens also work. Pass them in the Authorization: Bearer <token> header.

Request Shape

POST /api/v1/geofences/bulk
{
"items": [
{
"address": "123 Main St, San Francisco, CA",
"name": "SF Downtown Hub",
"buffer_meters": 100,
"tags": ["customer:acme", "tier:gold"]
},
{
"address": "456 Market St, San Francisco, CA"
}
],
"dedup_strategy": "skip"
}

Item fields

FieldTypeRequiredDescription
addressstringYesStreet address to geocode (max 500 chars)
namestringNoDisplay name (max 255 chars). Derived from address if omitted.
buffer_metersintNoFence radius in meters (1–10000, default 100)
tagsstring[]NoUp to 20 tag strings per item

dedup_strategy

Controls what happens when a geocoded point falls within an existing geofence's buffer zone:

StrategyBehavior
skip (default)Duplicates return status: "duplicate" with a dedup_match object. Nothing is written. Re-submitting the same batch is idempotent.
overrideDuplicates are created anyway. Results carry dedup_match so you know which were forced. Use this for intentional dual-fencing of the same location.
failFirst duplicate aborts the entire batch. HTTP 409 is returned with the partial result list. Use this for strict transactional flows.

Response Shape

All three strategies return the same response envelope. HTTP 201 means the batch was processed — some items may still have status: "error" or status: "duplicate". Check each result individually.

{
"results": [
{
"index": 0,
"status": "created",
"geofence_id": "550e8400-e29b-41d4-a716-446655440000"
},
{
"index": 1,
"status": "duplicate",
"dedup_match": {
"geofence_id": "660e8400-e29b-41d4-a716-446655440001",
"name": "Existing SF Hub",
"distance_meters": 12.3
}
},
{
"index": 2,
"status": "error",
"error_code": "GEOCODE_FAILED",
"error": {
"message": "Could not geocode address",
"code": "GEOCODE_FAILED"
}
}
]
}

index is 0-based and matches the position of the item in the request items array. For error results, error_code is the stable machine-readable key and mirrors the nested error.code field retained for existing clients.

HTTP status codes

HTTPMeaning
201Batch processed (mixed results possible — check each results[i].status)
401Missing or invalid auth
403Workspace lacks the BULK_IMPORT feature flag
409dedup_strategy: "fail" triggered — partial results in details.results
413Batch exceeds workspace cap (see Batch Size Cap)
422Request body invalid (malformed JSON, missing required fields)
429Rate limit exceeded (see Rate Limits)

Per-item error codes

CodeCause
GEOCODE_FAILEDGeocoder could not match the address. Check spelling and include city/state/ZIP.
GEOCODER_RATE_LIMITThe geocoder hit an upstream rate limit mid-batch. Retry the failed items after a short delay.
INVALID_TAGSTags failed validation: too long, invalid characters, or more than 20 per item.
CREATE_FAILEDCatch-all server error during geofence creation (rare). Retry the specific item.

curl Example

Minimal happy path with two addresses. The first is created, the second is a near-duplicate that triggers the default skip strategy:

curl -s -X POST https://api.spatialflow.io/api/v1/geofences/bulk \
-H "X-API-Key: sf_live_..." \
-H "Content-Type: application/json" \
-d '{
"items": [
{
"address": "1 Infinite Loop, Cupertino, CA",
"name": "Apple HQ",
"buffer_meters": 200,
"tags": ["customer:apple", "tier:enterprise"]
},
{
"address": "1 Apple Park Way, Cupertino, CA",
"name": "Apple Park"
}
]
}'

Example response:

{
"results": [
{
"index": 0,
"status": "created",
"geofence_id": "550e8400-e29b-41d4-a716-446655440000"
},
{
"index": 1,
"status": "duplicate",
"dedup_match": {
"geofence_id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Apple HQ",
"distance_meters": 483.2
}
}
]
}

Python SDK Example

The most common integration pattern is a two-pass import: detect duplicates with dedup_strategy: "skip", then force-create only the ones you intentionally want to dual-fence with dedup_strategy: "override".

import asyncio
from spatialflow import SpatialFlow
from spatialflow._generated.spatialflow_generated.models import (
BulkItemInput,
BulkCreateRequest,
)


async def sync_customer_sites(addresses: list[dict]) -> None:
async with SpatialFlow(api_key="sf_live_...") as client:
items = [
BulkItemInput(
address=row["address"],
name=row.get("name"),
tags=row.get("tags", []),
)
for row in addresses
]

# First pass: detect duplicates without writing
first_pass = await client.geofences.bulk_create(
BulkCreateRequest(items=items, dedup_strategy="skip")
)

created = [r for r in first_pass.results if r.status == "created"]
duplicates = [r for r in first_pass.results if r.status == "duplicate"]
errors = [r for r in first_pass.results if r.status == "error"]

print(f"First pass: {len(created)} created, {len(duplicates)} duplicate, {len(errors)} error")

# Log errors — these need address corrections before re-import
for r in errors:
print(f" Error at index {r.index}: {r.error.message} ({r.error.code})")

# Second pass: force-create duplicates the caller explicitly wants to override
# (e.g. a customer opened a second location at the same address)
duplicates_to_force = [items[r.index] for r in duplicates]
if duplicates_to_force:
override_pass = await client.geofences.bulk_create(
BulkCreateRequest(
items=duplicates_to_force,
dedup_strategy="override",
)
)
forced = [r for r in override_pass.results if r.status == "created"]
print(f"Override pass: {len(forced)} force-created")

# Collect all created geofence IDs
all_created_ids = [r.geofence_id for r in first_pass.results if r.status == "created"]
print(f"Total geofences created: {len(all_created_ids)}")


if __name__ == "__main__":
customer_sites = [
{"address": "123 Main St, San Francisco, CA", "tags": ["customer:acme"]},
{"address": "456 Market St, San Francisco, CA", "tags": ["customer:acme"]},
{"address": "789 Mission St, San Francisco, CA", "tags": ["customer:acme"]},
]
asyncio.run(sync_customer_sites(customer_sites))

Install the SDK:

pip install spatialflow

The BulkItemInput and BulkCreateRequest classes are generated from the OpenAPI spec and available in spatialflow._generated.spatialflow_generated.models. The client.geofences.bulk_create() method is the typed wrapper in GeofencesResource.

Rate Limits

The /bulk endpoint has dedicated rate limits separate from the general API limits:

Auth MethodLimit
JWT (browser session)10 requests/hour per workspace
API key60 requests/hour per workspace

With the default batch size cap of 500 items, an API key integration can create up to 30,000 geofences/hour (500 items × 60 requests/hour).

When the rate limit is exceeded, the API returns HTTP 429 with a Retry-After header indicating when you can retry:

{
"detail": "Rate limit exceeded. Try again in 3412 seconds.",
"error_code": "RATE_LIMIT_EXCEEDED"
}

Batch Size Cap

Each workspace has a configurable batch size limit (default: 500 items, maximum: 1000). Workspace owners can raise this from Dashboard → Workspace Settings → Bulk Create Limit. Raising above 1000 requires SpatialFlow support.

Requests above the cap are rejected with HTTP 413 — no items are processed:

{
"detail": "Batch too large: 501 > 500",
"error_code": "BATCH_TOO_LARGE",
"details": {
"max_items": 500,
"requested_items": 501
}
}

Split large imports into chunks on the client side and send them sequentially or with a short delay between batches to respect rate limits.

Other SDKs

  • Node SDKclient.geofences.appsGeofencesApiBulkCreateGeofences({ bulkCreateRequest: { items, dedup_strategy } }) → See the Node SDK README
  • Go SDKclient.BulkCreateGeofences(ctx, spatialflow.BulkCreateRequest{ Items: items, DedupStrategy: "skip" }) → See the Go SDK README

Both SDKs handle authentication, retries, and error mapping automatically.

OpenAPI Spec

The complete schema — including BulkCreateRequest, BulkCreateResponse, BulkResult, and BulkItemInput definitions — is published at specs/openapi-latest.json.

Pin to a specific version (e.g., openapi-v1.2.0.json) for stable client generation with openapi-generator:

openapi-generator generate \
-i https://raw.githubusercontent.com/spatialflow/spatialflow.io/main/specs/openapi-v1.2.0.json \
-g python \
-o ./my-spatialflow-client