API — AI Location Imports
The AI Location Import feature uses Google Gemini (gemini-2.5-flash-lite) to generate a draft set of locations from guided answers. The workflow is:
POST /ai-location-imports— create a batch and queue the generate job- Poll
GET /processes/{id}(or streamGET /processes/{id}/stream) untilstatus = waiting_review GET /ai-location-imports/{id}— review the draft itemsPATCH /ai-location-imports/{batchId}/items/{itemId}— approve, reject, or edit individual itemsPOST /ai-location-imports/{id}/import— apply approved items as real locations
See also: Processes API, AI Services, Integration Points: Next.js to Backend.
Middleware for all routes: auth:api, auth_session, audit
Batches are scoped to the requesting user’s own workspace. The user must own the workspace.
Batch statuses
| Status | Meaning |
|---|---|
draft | Generate complete, items pending review |
imported | All approvable items have been imported |
discarded | Soft-deleted via DELETE |
Item statuses
| Status | Meaning |
|---|---|
pending_review | Not yet acted on |
approved | Approved for import |
rejected | Manually rejected |
imported | Successfully written to the locations table |
failed | Import attempted but failed validation |
GET /ai-location-imports
List recent import batches for the authenticated user. Returns up to 20 batches ordered by updated_at descending. Excludes discarded batches.
Query parameters:
| Param | Type | Notes |
|---|---|---|
workspace_id | int | Filter by workspace |
Response 200: Array of batch resource objects (without items).
POST /ai-location-imports
Creates a new batch and dispatches GenerateAiLocationImportJob on the aigen queue. Returns immediately — the caller should watch the returned process ID.
Request body:
| Field | Type | Required | Notes |
|---|---|---|---|
workspace_id | int | yes | Must be a workspace owned by the user |
finder_id | int | no | If provided, imported locations are added to the finder’s backing map |
answers | object | yes | Guided Q&A answers (see below) |
answers.brand_name | string | no | max 160 |
answers.business_type | string | yes | max 120 |
answers.approximate_location_count | string | yes | e.g. "10-20", parsed for min/max target |
answers.geographic_scope | string | yes | max 1000 |
answers.customer_description | string | yes | max 2000 |
answers.common_tags | string | no | max 1000 |
answers.important_fields | string | no | max 1000 |
answers.example_locations | string | no | max 2000 |
answers.notes_to_avoid | string | no | max 1000 |
answers.source_notes | string | yes | max 4000; primary signal for Gemini |
Response 200:
{
"process": { "id": 42, "type": "ai_location_import_generate", "status": "queued" },
"batch": { "id": 7, "status": "draft", "total_items": 0 },
"next_action": "watch_process"
}⚠️ Warning: The generate job runs on the
aigenqueue. If the queue is not running or Gemini times out (90 s limit), the process transitions tofailed. Retry viaPOST /processes/{id}/retry.
GET /ai-location-imports/{id}
Returns a single batch with all items included.
Response 200: Batch resource with a nested items array.
Item shape:
{
"id": 101,
"batch_id": 7,
"status": "pending_review",
"confidence": "high",
"warnings": [],
"validation_errors": [],
"normalized_payload": {
"name": "Example Location",
"address": "123 Main St",
"city": "Canton",
"state": "OH",
"zip": "44720",
"country": "United States",
"phone": "(330) 555-1234",
"website": "https://example.com",
"lat": 40.798,
"lng": -81.376,
"tags": ["ice cream"],
"custom_fields": {}
},
"user_payload": null,
"raw_model_output": {}
}Confidence levels:
high— 6+ core fields filled, ≤1 warningmedium— some fields filledlow— validation errors present
PATCH /ai-location-imports/{batchId}/items/{itemId}
Updates the status and/or payload of a single draft item. Recalculates validation errors and syncs batch summary counts.
Request body:
| Field | Type | Required | Notes |
|---|---|---|---|
status | string | no | pending_review, approved, rejected |
payload | object | no | Merged over normalized_payload |
payload.name | string | no | max 255 |
payload.address | string | no | max 255 |
payload.city | string | no | max 120 |
payload.state | string | no | max 120 |
payload.zip | string | no | max 40 |
payload.country | string | no | max 120 |
payload.phone | string | no | max 120 |
payload.website | string | no | max 255 |
payload.tags | array of string | no | Each max 80 chars |
payload.custom_fields | object | no | |
payload.lat | float | no | |
payload.lng | float | no |
Setting status: approved while validation errors exist returns 422.
Response 200: Updated batch resource with all items.
POST /ai-location-imports/{id}/import
Triggers the apply phase. Any pending_review items that pass validation are auto-approved first. Then dispatches ApplyAiLocationImportJob synchronously on the ai_apply queue.
Returns 422 if there are no valid items to import.
If the batch has a finder_id, approved locations are added to that finder’s backing map. Geocoding (Google Maps Geocode API) is attempted for items missing coordinates; only ROOFTOP or RANGE_INTERPOLATED results are accepted.
Response 200:
{
"process": { "id": 43, "type": "ai_location_import_apply", "status": "completed" },
"batch": { "id": 7, "status": "imported", "imported_items": 9, "failed_items": 1 },
"next_action": "watch_process"
}⚠️ Warning: The apply job uses
dispatchSync, running in the web process and blocking the HTTP response. Expect 5–60 s for large batches. The execution budget is extended to 300 s viaset_time_limit.
DELETE /ai-location-imports/{id}
Soft-deletes the batch by setting status = discarded. Items remain in the database but the batch is excluded from all future queries.
Response 204: Empty body.
Batch resource shape
{
"id": 7,
"workspace_id": 3,
"finder_id": null,
"user_id": 1,
"status": "draft",
"model_name": "gemini-2.5-flash-lite",
"prompt_version": "v4",
"total_items": 10,
"approved_items": 3,
"rejected_items": 1,
"imported_items": 0,
"failed_items": 0,
"source_answers": {},
"usage_metadata": {
"prompt_token_count": 4120,
"candidates_token_count": 890,
"total_token_count": 5010
},
"created_at": "2026-04-27T09:00:00Z",
"updated_at": "2026-04-27T09:00:18Z"
}