> For the complete documentation index, see [llms.txt](https://api-docs.helloaviary.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://api-docs.helloaviary.ai/customers.md).

# Customers

Customer records and their dynamic data (customers + customer\_data + customer\_data\_mapping). External callers see friendly-keyed JSON decoded through the per-(client\_id, use\_case\_id) mapping; internal callers see the raw `string_column_N`/`int_column_N` shape.

## Create a customer (external)

> Creates a \`customers\` row plus a \`customer\_data\` row keyed by the\
> same (client\_id, use\_case\_id), and kicks off auto-scheduling for the\
> client asynchronously.\
> \
> \`client\_id\` is resolved from the API key — DO NOT include it in the\
> body. \`use\_case\_id\` is required. Friendly-keyed dynamic columns go\
> under \`data\` and must already be present in the\
> \`customer\_data\_mapping\` for this (client\_id, use\_case\_id) — for new\
> mappings, use the legacy bulk upload path first.<br>

```json
{"openapi":"3.0.3","info":{"title":"Aviary v1 API (merged — Osprey v1 + legacy clientApi)","version":"1.0.0"},"tags":[{"name":"Customers","description":"Customer records and their dynamic data (customers + customer_data +\ncustomer_data_mapping). External callers see friendly-keyed JSON\ndecoded through the per-(client_id, use_case_id) mapping; internal\ncallers see the raw `string_column_N`/`int_column_N` shape.\n"}],"servers":[{"url":"https://osprey.helloaviary.com/api","description":"Production"},{"url":"https://osprey-dev.helloaviary.com/api","description":"Development"}],"security":[{"ApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-Api-Key","description":"API key for authentication"}},"schemas":{"CustomerCreateRequest":{"type":"object","required":["use_case_id"],"description":"Body for POST /v1/customers.\n\n`client_id` is intentionally absent — it's resolved from the API key.\n`data` carries the friendly-keyed dynamic columns; reserved customer\nkeys (first_name, phone_number, etc.) must be at the top level (and\nwill be rejected if nested inside `data`).\n","properties":{"use_case_id":{"type":"integer","format":"int64"},"first_name":{"type":"string"},"last_name":{"type":"string"},"phone_number":{"type":"string"},"state":{"type":"string"},"do_not_call":{"type":"boolean"},"allow_multiple_calls_per_day":{"type":"boolean"},"data":{"type":"object","additionalProperties":true,"description":"Friendly-keyed dynamic columns matching the existing customer_data_mapping for this (client_id, use_case_id)."}}},"CustomerExternalResponse":{"type":"object","required":["data","meta"],"properties":{"data":{"$ref":"#/components/schemas/CustomerExternal"},"meta":{"$ref":"#/components/schemas/Meta"}}},"CustomerExternal":{"type":"object","required":["id","external_id","allow_multiple_calls_per_day","data","created_at","updated_at"],"description":"External customer projection. The dynamic columns from `customer_data`\nare decoded through the per-(client_id, use_case_id) mapping and\nreturned under `data`. Reserved customer keys (id, first_name, etc.)\nlive at the top level so they can never collide with mapping keys.\n","properties":{"id":{"type":"integer","format":"int64"},"external_id":{"type":"string","format":"uuid"},"use_case_id":{"type":"integer","format":"int32","nullable":true},"first_name":{"type":"string","nullable":true},"last_name":{"type":"string","nullable":true},"phone_number":{"type":"string","nullable":true},"state":{"type":"string","nullable":true,"description":"2-letter state code."},"do_not_call":{"type":"boolean","nullable":true},"do_not_call_date_requested":{"type":"string","format":"date-time","nullable":true},"allow_multiple_calls_per_day":{"type":"boolean"},"data":{"type":"object","additionalProperties":true,"description":"Friendly-keyed dynamic data for this customer. Keys come from\n`customer_data_mapping.string_column_N_name` for the customer's\n(client_id, use_case_id). Empty/zero string and date cells are\nomitted; numeric zeroes are kept (a real \"0\" balance is a valid\nvalue).\n"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"Meta":{"type":"object","required":["request_id"],"properties":{"request_id":{"type":"string","format":"uuid","description":"Unique ID for this request. Echoed in the `X-Request-ID` response\nheader. Send this back to support to debug a specific call.\n"}}},"ErrorEnvelope":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message","request_id"],"properties":{"code":{"$ref":"#/components/schemas/ErrorCode"},"message":{"type":"string"},"request_id":{"type":"string","format":"uuid"}}}}},"ErrorCode":{"type":"string","enum":["bad_request","unauthorized","forbidden","not_found","conflict","internal_error","field_not_editable","call_already_started"]}}},"paths":{"/v1/customers":{"post":{"summary":"Create a customer (external)","description":"Creates a `customers` row plus a `customer_data` row keyed by the\nsame (client_id, use_case_id), and kicks off auto-scheduling for the\nclient asynchronously.\n\n`client_id` is resolved from the API key — DO NOT include it in the\nbody. `use_case_id` is required. Friendly-keyed dynamic columns go\nunder `data` and must already be present in the\n`customer_data_mapping` for this (client_id, use_case_id) — for new\nmappings, use the legacy bulk upload path first.\n","operationId":"postCustomerExternalV1","tags":["Customers"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerCreateRequest"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerExternalResponse"}}}},"400":{"description":"Bad body, missing use_case_id, or reserved key under `data`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Missing or invalid X-Api-Key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Ingestion error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get a customer (external, decoded data)

> Returns the safe-for-external customer projection. The dynamic\
> columns from the latest \`customer\_data\` row are decoded through\
> \`customer\_data\_mapping\` and returned under \`data\` with friendly keys.<br>

```json
{"openapi":"3.0.3","info":{"title":"Aviary v1 API (merged — Osprey v1 + legacy clientApi)","version":"1.0.0"},"tags":[{"name":"Customers","description":"Customer records and their dynamic data (customers + customer_data +\ncustomer_data_mapping). External callers see friendly-keyed JSON\ndecoded through the per-(client_id, use_case_id) mapping; internal\ncallers see the raw `string_column_N`/`int_column_N` shape.\n"}],"servers":[{"url":"https://osprey.helloaviary.com/api","description":"Production"},{"url":"https://osprey-dev.helloaviary.com/api","description":"Development"}],"security":[{"ApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-Api-Key","description":"API key for authentication"}},"schemas":{"CustomerExternalResponse":{"type":"object","required":["data","meta"],"properties":{"data":{"$ref":"#/components/schemas/CustomerExternal"},"meta":{"$ref":"#/components/schemas/Meta"}}},"CustomerExternal":{"type":"object","required":["id","external_id","allow_multiple_calls_per_day","data","created_at","updated_at"],"description":"External customer projection. The dynamic columns from `customer_data`\nare decoded through the per-(client_id, use_case_id) mapping and\nreturned under `data`. Reserved customer keys (id, first_name, etc.)\nlive at the top level so they can never collide with mapping keys.\n","properties":{"id":{"type":"integer","format":"int64"},"external_id":{"type":"string","format":"uuid"},"use_case_id":{"type":"integer","format":"int32","nullable":true},"first_name":{"type":"string","nullable":true},"last_name":{"type":"string","nullable":true},"phone_number":{"type":"string","nullable":true},"state":{"type":"string","nullable":true,"description":"2-letter state code."},"do_not_call":{"type":"boolean","nullable":true},"do_not_call_date_requested":{"type":"string","format":"date-time","nullable":true},"allow_multiple_calls_per_day":{"type":"boolean"},"data":{"type":"object","additionalProperties":true,"description":"Friendly-keyed dynamic data for this customer. Keys come from\n`customer_data_mapping.string_column_N_name` for the customer's\n(client_id, use_case_id). Empty/zero string and date cells are\nomitted; numeric zeroes are kept (a real \"0\" balance is a valid\nvalue).\n"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"Meta":{"type":"object","required":["request_id"],"properties":{"request_id":{"type":"string","format":"uuid","description":"Unique ID for this request. Echoed in the `X-Request-ID` response\nheader. Send this back to support to debug a specific call.\n"}}},"ErrorEnvelope":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message","request_id"],"properties":{"code":{"$ref":"#/components/schemas/ErrorCode"},"message":{"type":"string"},"request_id":{"type":"string","format":"uuid"}}}}},"ErrorCode":{"type":"string","enum":["bad_request","unauthorized","forbidden","not_found","conflict","internal_error","field_not_editable","call_already_started"]}}},"paths":{"/v1/customers/{id}":{"get":{"summary":"Get a customer (external, decoded data)","description":"Returns the safe-for-external customer projection. The dynamic\ncolumns from the latest `customer_data` row are decoded through\n`customer_data_mapping` and returned under `data` with friendly keys.\n","operationId":"getCustomerExternalV1","tags":["Customers"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","format":"int64"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerExternalResponse"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Missing or invalid X-Api-Key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Update a customer (external, mapping-aware)

> Partial update. Top-level fields use a strict allowlist\
> (first\_name, last\_name, phone\_number, state, do\_not\_call,\
> allow\_multiple\_calls\_per\_day) — anything else returns 400\
> \`field\_not\_editable\`.\
> \
> The \`data\` object is decoded via \`customer\_data\_mapping\` for the\
> customer's (client\_id, use\_case\_id). Friendly keys not in the\
> mapping return 400 \`field\_not\_editable\`. Type mismatches return\
> 400\. Updates target the LATEST \`customer\_data\` row only — older\
> per-call snapshots are left untouched.<br>

```json
{"openapi":"3.0.3","info":{"title":"Aviary v1 API (merged — Osprey v1 + legacy clientApi)","version":"1.0.0"},"tags":[{"name":"Customers","description":"Customer records and their dynamic data (customers + customer_data +\ncustomer_data_mapping). External callers see friendly-keyed JSON\ndecoded through the per-(client_id, use_case_id) mapping; internal\ncallers see the raw `string_column_N`/`int_column_N` shape.\n"}],"servers":[{"url":"https://osprey.helloaviary.com/api","description":"Production"},{"url":"https://osprey-dev.helloaviary.com/api","description":"Development"}],"security":[{"ApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-Api-Key","description":"API key for authentication"}},"schemas":{"CustomerPatchRequest":{"type":"object","description":"Body for PATCH /v1/customers/:id. Empty body returns 400. Unknown\ntop-level keys return 400 `field_not_editable`. Friendly keys inside\n`data` that aren't in the mapping return 400 `field_not_editable`.\n","properties":{"first_name":{"type":"string","nullable":true},"last_name":{"type":"string","nullable":true},"phone_number":{"type":"string","nullable":true},"state":{"type":"string","nullable":true},"do_not_call":{"type":"boolean","nullable":true},"allow_multiple_calls_per_day":{"type":"boolean"},"data":{"type":"object","additionalProperties":true,"description":"Mapping-aware partial update of the latest customer_data row.\nSending an explicit null for a key is silently skipped — varchar\ncolumns can't be reset to empty via PATCH (use DELETE + recreate\nfor that flow if needed).\n"}}},"CustomerExternalResponse":{"type":"object","required":["data","meta"],"properties":{"data":{"$ref":"#/components/schemas/CustomerExternal"},"meta":{"$ref":"#/components/schemas/Meta"}}},"CustomerExternal":{"type":"object","required":["id","external_id","allow_multiple_calls_per_day","data","created_at","updated_at"],"description":"External customer projection. The dynamic columns from `customer_data`\nare decoded through the per-(client_id, use_case_id) mapping and\nreturned under `data`. Reserved customer keys (id, first_name, etc.)\nlive at the top level so they can never collide with mapping keys.\n","properties":{"id":{"type":"integer","format":"int64"},"external_id":{"type":"string","format":"uuid"},"use_case_id":{"type":"integer","format":"int32","nullable":true},"first_name":{"type":"string","nullable":true},"last_name":{"type":"string","nullable":true},"phone_number":{"type":"string","nullable":true},"state":{"type":"string","nullable":true,"description":"2-letter state code."},"do_not_call":{"type":"boolean","nullable":true},"do_not_call_date_requested":{"type":"string","format":"date-time","nullable":true},"allow_multiple_calls_per_day":{"type":"boolean"},"data":{"type":"object","additionalProperties":true,"description":"Friendly-keyed dynamic data for this customer. Keys come from\n`customer_data_mapping.string_column_N_name` for the customer's\n(client_id, use_case_id). Empty/zero string and date cells are\nomitted; numeric zeroes are kept (a real \"0\" balance is a valid\nvalue).\n"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"Meta":{"type":"object","required":["request_id"],"properties":{"request_id":{"type":"string","format":"uuid","description":"Unique ID for this request. Echoed in the `X-Request-ID` response\nheader. Send this back to support to debug a specific call.\n"}}},"ErrorEnvelope":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message","request_id"],"properties":{"code":{"$ref":"#/components/schemas/ErrorCode"},"message":{"type":"string"},"request_id":{"type":"string","format":"uuid"}}}}},"ErrorCode":{"type":"string","enum":["bad_request","unauthorized","forbidden","not_found","conflict","internal_error","field_not_editable","call_already_started"]}}},"paths":{"/v1/customers/{id}":{"patch":{"summary":"Update a customer (external, mapping-aware)","description":"Partial update. Top-level fields use a strict allowlist\n(first_name, last_name, phone_number, state, do_not_call,\nallow_multiple_calls_per_day) — anything else returns 400\n`field_not_editable`.\n\nThe `data` object is decoded via `customer_data_mapping` for the\ncustomer's (client_id, use_case_id). Friendly keys not in the\nmapping return 400 `field_not_editable`. Type mismatches return\n400. Updates target the LATEST `customer_data` row only — older\nper-call snapshots are left untouched.\n","operationId":"patchCustomerExternalV1","tags":["Customers"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","format":"int64"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerPatchRequest"}}}},"responses":{"200":{"description":"OK — returns the post-update customer projection.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerExternalResponse"}}}},"400":{"description":"Bad body, unknown field, type mismatch, or missing mapping","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Missing or invalid X-Api-Key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Customer not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"No `customer_data` row exists yet to patch.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```

## Get a customer (internal, raw column shape)

> Returns every column on \`customers\` plus the latest raw\
> \`customer\_data\` row with \`string\_column\_N\`/\`int\_column\_N\`/etc.\
> intact. The mapping is NOT applied — internal callers debugging\
> ingestion want to see the literal cell contents.<br>

```json
{"openapi":"3.0.3","info":{"title":"Aviary v1 API (merged — Osprey v1 + legacy clientApi)","version":"1.0.0"},"tags":[{"name":"Customers","description":"Customer records and their dynamic data (customers + customer_data +\ncustomer_data_mapping). External callers see friendly-keyed JSON\ndecoded through the per-(client_id, use_case_id) mapping; internal\ncallers see the raw `string_column_N`/`int_column_N` shape.\n"},{"name":"Internal","description":"Internal-only endpoints. Forbidden for external client keys."}],"servers":[{"url":"https://osprey.helloaviary.com/api","description":"Production"},{"url":"https://osprey-dev.helloaviary.com/api","description":"Development"}],"security":[{"ApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-Api-Key","description":"API key for authentication"}},"schemas":{"CustomerInternalResponse":{"type":"object","required":["data","meta"],"description":"Raw internal projection — every column on `customers` plus the latest\nraw `customer_data` row. The dynamic columns are NOT decoded; this\nendpoint is for debugging ingestion.\n","properties":{"data":{"type":"object","required":["customer"],"properties":{"customer":{"type":"object","additionalProperties":true,"description":"Full row from `customers`."},"customer_data":{"type":"object","additionalProperties":true,"nullable":true,"description":"Latest raw row from `customer_data`, or null if none exists yet."}}},"meta":{"$ref":"#/components/schemas/Meta"}}},"Meta":{"type":"object","required":["request_id"],"properties":{"request_id":{"type":"string","format":"uuid","description":"Unique ID for this request. Echoed in the `X-Request-ID` response\nheader. Send this back to support to debug a specific call.\n"}}},"ErrorEnvelope":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message","request_id"],"properties":{"code":{"$ref":"#/components/schemas/ErrorCode"},"message":{"type":"string"},"request_id":{"type":"string","format":"uuid"}}}}},"ErrorCode":{"type":"string","enum":["bad_request","unauthorized","forbidden","not_found","conflict","internal_error","field_not_editable","call_already_started"]}}},"paths":{"/internal/v1/customers/{id}":{"get":{"summary":"Get a customer (internal, raw column shape)","description":"Returns every column on `customers` plus the latest raw\n`customer_data` row with `string_column_N`/`int_column_N`/etc.\nintact. The mapping is NOT applied — internal callers debugging\ningestion want to see the literal cell contents.\n","operationId":"getCustomerInternalV1","tags":["Customers","Internal"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","format":"int64"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerInternalResponse"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Missing or invalid X-Api-Key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"External key cannot reach internal scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://api-docs.helloaviary.ai/customers.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
