AgentStack Docs

REST API: contacts

Create, inspect, export, and maintain contact records.

Contacts are organization records. The REST API can create, list, inspect, export, and delete a contact. Use a stable external identifier in your own system. Treat the AgentStack contact identifier that the API returns as the key for later reads or deletions. Make a contact export an explicit operator job with a defined retention destination. Do not export contacts automatically after every conversation.

The agent-specific verification and resume settings control how an embedded conversation can connect to a verified contact. When you enable or regenerate verification, the API returns secret material. Store the secret material only in a protected server-side secret manager. After you regenerate the secret, rotate your integrations. Never return the secret material to a browser. The runtime configuration includes guest-use behavior. Clients must not rely on undocumented response fields.

A contact deletion is a consequential data action. Verify the identity of the contact. Record the reason in your system. Follow the response envelope. For details about the widget JWT and the visitor experience, read Identity verification. For changes to contact policy, use the exact request shapes in the API reference.

Contacts of an agent that waits for deletion

A contact belongs to one agent. While that agent waits for deletion, its contacts are frozen. DELETE /contacts/{id} answers 410 AGENT_UNAVAILABLE, and the contact row stays. The agent-specific verification and resume-window routes answer the same 410. Call POST /agents/{id}/cancel-delete first, then retry.

POST /contacts reports the same agent as 404 AGENT_NOT_FOUND, not as 410. The two doors differ on purpose, so match on both codes when you handle an agent that waits for deletion.

Reads stay open while the agent waits. GET /contacts and GET /contacts/{id} still return the contact, in the same shape. POST /contacts/export still includes its rows, so you can take the data out before the agent is purged. An export with an agentId from another organization answers 404 AGENT_NOT_FOUND.

Background contacts exports

POST /contacts/export builds the CSV inside the request and stops at 50,000 rows. For a larger export, or one you want to collect later, use a background export. POST /contacts/exports takes the same filters (agentId, status, source, search, and scoreRange) and answers 201 with { exportId, status: "pending" }. The export has no row cap. Only one contacts export runs in an organization at a time, so a second request answers 409 CONFLICT until the first one finishes. An agentId from another organization answers 404 AGENT_NOT_FOUND. The request needs an API key with the write scope.

Poll GET /contacts/exports/{exportId} until status is completed or failed. The response also has createdAt, completedAt, expiresAt, fileSize in bytes, errorMessage, and the filters the export used. When the export is completed and not expired, downloadUrl is a presigned link to the CSV. The link works for up to 1 hour, and each read mints a new one, so fetch the file soon after you read the link. The file stays available for 7 days after the export completes, until expiresAt. After that the status is expired, downloadUrl is null, and a daily job deletes the file. An id from another organization answers 404.

GET /contacts/exports lists the organization's contacts exports, newest first, in the same shape. Page it with limit (at most 50) and after, the nextCursor of the previous page. The three endpoints need the Contacts export permission. A key that can read contacts with that permission already gets the whole CSV from POST /contacts/export, so the read endpoints include the link for any such key. The Exports page in the dashboard shows the same exports.

Limits on custom attributes and Stripe customer ids

POST /contacts checks the size of customAttributes and stripeCustomerIds, and answers 400 VALIDATION_ERROR when a request passes a limit. Nothing is stored, and no key is dropped for you. Before this check the API stored a request of any size, so an integration that sends large objects now gets a 400.

  • customAttributes holds at most 20 keys. A key has at most 100 characters. A value is a string of at most 2,000 characters, a finite number or a boolean. An object, an array, null or a number too large to store (such as 1e400) as a value is refused.
  • The upsert merges customAttributes into the attributes the contact already has. The limit of 20 keys applies to the merged object, so a request that adds a new key to a contact that already holds 20 keys answers 400. A request that only changes keys the contact already has still succeeds. A contact stored with more than 20 keys before this limit keeps its keys and can still change them, but it cannot gain a new key.
  • stripeCustomerIds replaces the stored list. It holds at most 20 ids, and an id has at most 255 characters.

The same limits apply to every other way a contact gets attributes, but those ways drop what is over a limit instead of refusing it. A lead form or Lead Collection submit on a contact that already holds 20 keys is still saved, and its new keys are dropped. (The lead form still refuses a single key or value that is too long, as it always has.) A widget identity token is still accepted when its custom_attributes or stripe_accounts pass a limit: a value over a limit is dropped whole, and only the first 20 keys and the first 20 Stripe ids that pass are kept. The token's email (at most 320 characters), name (200) and phonenumber (30) claims are dropped whole when they are too long, and the token is still accepted. A user_id longer than 500 characters refuses the token, as a missing user_id does, because it is the contact's key and is never shortened. An inbound email whose sender name is longer than 200 characters creates the contact without a name. The limits matter because a verified contact's attributes go into the agent's instructions on every chat turn.

On this page