POST /v1/deep-research
https://api.voygr.tech/v1/deep-research
Overview
Research shared attributes for a batch of places, returning values, confidence tiers, and statuses. Unavailable values are null; individual failures return error rows without failing the batch. If research is unavailable, the response is still 200 with abstained rows and meta.stopped_reason=agent_unavailable; nothing is billed. Ordinary researched fields, including abstentions, use the standard per-field rate. Requires a key permitted to use research.
Verifying the name and address you sent (verify_identity)
Set verify_identity: true when your records may be close but not exact - an
abbreviation, an initialism, a store number, or a slightly stale spelling. We
settle what the business is actually called and where it actually is BEFORE
researching anything else, and every attribute you asked for is then researched
against the verified values rather than the ones you sent.
Each row carries an identity block giving, for the name and the address, the
value we settled on, a verdict, and your original. The verdict is confirmed
(yours was already right), corrected (we replaced it) or unresolved (we could
not establish it, so your value comes back untouched). value is always what the
rest of the row was researched against, so branch on the verdict rather than
comparing strings.
Formatting is not a correction: punctuation, casing, "and" vs "&", "St" vs
"Street" and a dropped or added Inc/Ltd/LLC all come back confirmed with your
own string.
This is not a postal validation - a confirmed address matches what the business
publishes about itself, not a postal database - and it makes no claim about
whether the business is still open.
Costs a flat 1 extra credit per row, independent of how many attributes you requested.
Owner discovery (owner_person, owner_org)
Request these as ordinary field specs. owner_person is the individual owner,
co-owners or franchisee; multiple owners are comma-separated in one string.
owner_org is the parent organisation when no individual owner exists.
They are mutually exclusive per place: when one is established, the other
abstains. "no individual owner" is a legitimate answer.
Results may be confirmed (high or medium confidence), estimated (low confidence, treat as a lead), or unconfirmed (value is null). Unavailable ownership is a normal result; do not interpret an abstention as a claim that no owner exists. Owner fields use the same billing rules as other fields on this endpoint.
Authentication
- API key in header
X-API-Key: Your customer API key (pk_live_...), sent on every request. Self-serve at https://api.voygr.tech/checkout.
Request body
required
application/json
Schema: DeepResearchRequest
Responses
200
Successful Response
application/json
Schema: DeepResearchResponse
401
Missing or invalid X-API-Key.
application/json
X-API-Key header absent
{
"detail": {
"error": "API key required"
}
}X-API-Key doesn't resolve to an active key
{
"detail": {
"error": "invalid API key"
}
}402
Not enough credits, or the estimate exceeds the caller's ceiling.
application/json
Insufficient available credits
{
"detail": {
"error": "quota_exceeded",
"needed_credits": 120,
"checkout_url": "/checkout/buy"
}
}Up-front estimate exceeds request maxcredits (nothing charged)
{
"detail": {
"success": false,
"error": "max_credits_exceeded",
"detail": "Estimated cost 120 credits exceeds max_credits 100; nothing was run or charged.",
"estimated_credits": 120
}
}403
This key is not permitted to use research.
application/json
notentitled
{
"detail": {
"error": "endpoint not permitted for this key"
}
}422
An attribute asks for something the service does not research. Remove or rephrase the named attributes and resend; nothing is charged.
application/json
Schema: object
Fields
| Field | Type | Required | Description and constraints |
|---|---|---|---|
success | boolean | required | |
error | string | required | |
error_code | string | required | |
rejected_fields | object mapping names to string | required | Attribute name to the reason it was refused. |
request_id | string | null | optional | |
validation_timestamp | string | optional |
fieldrejected
{
"success": false,
"error": "field_rejected_by_policy",
"error_code": "FIELD_REJECTED",
"rejected_fields": {
"alarm_code": "harmful"
},
"request_id": "example-request-id",
"validation_timestamp": "2026-09-16T12:00:00Z"
}400
Invalid request. Correct the fields before retrying.
application/json
Schema: object | object
Option 1 fields
| Field | Type | Required | Description and constraints | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
success | boolean | required | constant False | ||||||||||||||||
error | string | required | |||||||||||||||||
error_code | string | required | constant VALIDATION_ERROR | ||||||||||||||||
request_id | string | null | required | |||||||||||||||||
validation_timestamp | string | required | |||||||||||||||||
detail | array of object | required | Item fields
|
Option 2 fields
| Field | Type | Required | Description and constraints | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
detail | object | required | Gateway rejection, for example an invalid Idempotency-Key header.Fields
|
One of
- Option 1: requires
success,error,error_code,request_id,validation_timestamp,detail - Option 2: requires
detail
invalidinput
{
"success": false,
"error": "Invalid input provided",
"error_code": "VALIDATION_ERROR",
"request_id": "example-request-id",
"validation_timestamp": "2026-09-16T12:00:00Z",
"detail": [
{
"field": "pois.0.poi_name",
"message": "Field required",
"type": "missing"
}
]
}Schemas
DeepResearchRequest
Type: object
Research a shared list of attributes for a batch of places. A single place is a list of one. Individual row failures do not fail the whole request.
| Field | Type | Required | Description and constraints |
|---|---|---|---|
pois | array of DeepResearchPOI | required | Places to enrich (batch). Single-POI = a list of one. 1 to 10 items |
attributes | array of DeepResearchFieldSpec | required | Attributes to research for every place. Unsupported requests can return unavailable results. 1 to 20 items |
validate_existence | boolean | optional | Also validate whether each place exists and return an existence attribute per row. Default false. default False |
verify_identity | boolean | optional | If true, verify the name and address you supplied before researching anything else, and research every requested attribute against the verified values rather than the ones you sent. Each row returns an identity block giving, for name and address, the value we settled on, a verdict (confirmed | corrected | unresolved) and your original. Costs a flat 1 extra credit per row, independent of attribute count. Default false.default False |
estimate_only | boolean | optional | Estimate without research or charges. Returns meta.estimated_credits for all requested rows and an empty rows list. Default false. default False |
sample | integer (min 1.0) | null | optional | Cost preview: process only the first N POIs (a cheap subset) and bill only those N rows. Omit to process the whole list. Precedence: estimate_only (no run) > sample (subset) > full. |
max_credits | integer (min 0.0) | null | optional | Per-request credit ceiling. If the up-front estimate (for the rows that would run - the sampled subset when sample is set, else all rows) exceeds this, the request is REJECTED with 402 before running and nothing is charged. The estimate is still reported. |
Example
{
"pois": [
{
"id": "bakery-001",
"poi_name": "Example Bakery",
"address": "123 Example Street",
"country": "US",
"latitude": 37.7749,
"longitude": -122.4194
},
{
"id": "cafe-002",
"poi_name": "Example Cafe",
"address": "456 Sample Avenue",
"country": "US",
"latitude": 37.7755,
"longitude": -122.4183
}
],
"attributes": [
{
"name": "outdoor_seating",
"description": "Does the place have outdoor seating?",
"value_type": "bool"
},
{
"name": "price_band",
"description": "Typical price level",
"value_type": "enum",
"allowed_values": [
"cheap",
"mid",
"expensive"
]
}
]
}DeepResearchResponse
Type: object
Research results in request order. Each field returns a value, confidence tier, and status; unavailable values are null. A per-place failure is a row with status=error, allowing other rows to succeed.
| Field | Type | Required | Description and constraints |
|---|---|---|---|
success | boolean | required | Whether the request ran (individual rows/attributes may still be null or errored) |
rows | array of DeepResearchRow | optional | Per-POI results (matrix), in request order |
meta | DeepResearchMeta | null | optional | Request-level aggregate bookkeeping (see DeepResearchMeta) |
enrichment_timestamp | string | null | optional | UTC timestamp (ISO 8601) |
error | string | null | optional | Error message if success=false |
error_code | string | null | optional | Error category code |
request_id | string | null | optional | Request ID for tracing |
Example 1
{
"success": true,
"rows": [
{
"id": "poi-12345",
"poi_name": "Zuni Cafe",
"status": "enriched",
"outcome": "enriched",
"fields": {
"phone": {
"value": "+14155551234",
"confidence": "high",
"status": "confirmed"
},
"outdoor_seating": {
"value": true,
"confidence": "medium",
"status": "confirmed"
}
}
},
{
"id": "poi-67890",
"poi_name": "Tartine Bakery",
"status": "abstain",
"outcome": "abstain",
"fields": {
"outdoor_seating": {
"status": "unconfirmed"
}
}
}
],
"meta": {
"stopped_reason": "completed"
},
"enrichment_timestamp": "2026-06-29T12:34:56Z",
"request_id": "ad33009a-fdfe-4e25-9650-4a49099c3d4a"
}DeepResearchPOI
Type: object
One place to research. Provide structured address fields, an unstructured address (also accepted as freeform_address), or coordinates. The attributes to research are shared across the request.
| Field | Type | Required | Description and constraints |
|---|---|---|---|
id | string (max length 100) | null | optional | Client-provided identifier (keys the response row) |
poi_name | string | required | Place name length 1 to 200 |
street | string | null | optional | Street address |
city | string | null | optional | City |
region | string | null | optional | State/province |
postcode | string | null | optional | Postal/ZIP code |
country | string | null | optional | 2-letter ISO country code (e.g. US) locating the place. |
address | string | null | optional | Unstructured address string. May also be sent as freeform_address (input-only alias). |
latitude | number (-90.0 to 90.0) | null | optional | Latitude in decimal degrees (WGS84). |
longitude | number (-180.0 to 180.0) | null | optional | Longitude in decimal degrees (WGS84). Provide with latitude. |
DeepResearchFieldSpec
Type: object
One requested research field. The name identifies the field; description defines the requested information and is treated as untrusted data, never instructions.
| Field | Type | Required | Description and constraints |
|---|---|---|---|
name | string | required | Field identifier, e.g. wheelchair_accessible. owner_person identifies individual owners or franchisees; owner_org identifies a parent organisation when there is no individual owner. These owner fields are mutually exclusive per place. length 1 to 100 |
description | string (max length 500) | null | optional | Natural-language definition of the field (treated as untrusted data) |
value_type | string | null | optional | Expected value type hint: bool | enum | string | number |
allowed_values | array of string | null | optional | For enum fields, the permitted values |
DeepResearchRow
Type: object
One POI's result in a batch: the researched attributes + a row-level status.
A row NEVER fails the request: on a per-POI error it carries
status="error" + error, so callers get partials for the rest.
| Field | Type | Required | Description and constraints |
|---|---|---|---|
id | string | null | optional | Client-provided POI id (pass-through) |
poi_name | string | null | optional | Place name (echoed from the request) |
identity | ResolvedIdentity | null | optional | Verified name and address - present only when verify_identity=true. Every other attribute on this row was researched against these values. |
status | string | required | Row outcome: enriched | abstain | error | cost_capped |
outcome | string (one of enriched, abstain) | null | optional | 'enriched' if any attribute was confirmed, else 'abstain' (null on error) |
fields | object mapping names to ResultCell | null | optional | Attribute name -> result (null when the row errored) |
error | string | null | optional | Safe error message; set ONLY when the row failed |
DeepResearchMeta
Type: object
Request outcome and estimated credit cost.
| Field | Type | Required | Description and constraints |
|---|---|---|---|
stopped_reason | string | null | optional | Why the run stopped: completed | cost_capped | agent_unavailable | estimate_only |
estimated_credits | integer | null | optional | Cost estimate for all requested rows before sampling or capping. Present on every response, including estimate_only requests. |
ResolvedIdentity
Type: object
Per-row result of verify_identity. Present only when it was requested.
| Field | Type | Required | Description and constraints |
|---|---|---|---|
name | ResolvedIdentityField | required | The business name we settled on |
address | ResolvedIdentityField | required | The address we settled on |
ResultCell
Type: object
One returned value, confidence tier, and status, returned by enrichment in cell format. Unavailable values are null.
| Field | Type | Required | Description and constraints |
|---|---|---|---|
value | any | null | optional | The field value; null when unconfirmed |
confidence | string | null | optional | Trust tier: high | medium | low; null when value is null |
status | string | required | confirmed | estimated | unconfirmed | cost_capped | rejected. Estimated values have low confidence; filter by status if you need confirmed values only. |
ResolvedIdentityField
Type: object
One half of a verified identity: the name, or the address.
value is what we settled on and what the rest of the research ran against
- the corrected string when we corrected it, and the caller's own string
when we confirmed it or could not establish it.
originalis always what the caller sent, so a consumer can branch on the verdict instead of diffing strings (is "St." -> "Street" a change worth persisting? they can't tell).
Deliberately NO confidence, evidence URLs, source excerpts, candidates or method: those are computed to DECIDE the verdict and stay internal. The verdict is the entire public surface.
| Field | Type | Required | Description and constraints |
|---|---|---|---|
value | string | null | optional | The value we settled on and researched against |
verdict | string | required | confirmed = yours was already right; corrected = we replaced it; unresolved = we could not establish it and left yours untouched one of confirmed, corrected, unresolved |
original | string | null | optional | The value you supplied, echoed back unchanged |
On this page
