Notrus
Beta

Importação de cases

A API GraphQL da Notrus expõe as conversas da sua organização e seu estado de classificação, recebe o envio de documentos e a importação de cases. Este portal documenta todas as operações e permite testá-las ao vivo no seu navegador.

A operação

A importação usa o mesmo endpoint GraphQL e a mesma chave de API das consultas, no cabeçalho apikey (veja Autenticação). A organização vem da chave: o input não tem campo de organização.

endpoint
https://api.hom.notrus.ai/graphql
graphql
mutation ImportCase($input: ImportCaseInput!) {
importCase(input: $input) {
outcome
case { id code outcome }
customer { id outcome }
conversations {
conversationId
threads {
id
outcome
active
traces { inserted duplicated missingFromPayload }
tags { linked replaced tagsCreated valuesCreated }
}
}
warnings
}
}

Os tipos ImportCaseInput e ImportCaseResult estão na Referência do schema, e a mutation pode ser executada no Console da API.

Estrutura do input

O input é o case. Dentro dele vêm as conversas que fizeram parte do atendimento, cada uma com as suas threads e as mensagens de cada thread:

text
ImportCaseInput caseId · status · resolution · assignedUserName · transferredAt · assumedAt · closedAt · customerData
└─ conversations[] conversationId · channelType
└─ threads[] threadId
├─ tags[] reference · value · label
└─ messages[] sentAt · role · text · hasMedia · authorIdentifier
  • ImportCaseInput: o atendimento, com protocolo, status e datas. A Notrus gera o código legível (CS-0042).
  • ImportCaseConversationInput: um canal, como um WhatsApp ou um endereço de e-mail. Um case pode ter várias conversas.
  • ImportCaseThreadInput: um trecho do atendimento dentro daquela conversa. Uma conversa pode ter várias threads no mesmo case.

Exemplo

As variáveis de um case encerrado, atendido por WhatsApp e por e-mail. A requisição é um POST com query (a mutation acima) e variables:

json
{
"input": {
"caseId": "protocolo-777",
"status": "ENCERRADO",
"resolution": "RESOLVIDO",
"assignedUserName": "Marina Souza",
"transferredAt": "2026-10-15T09:02:00-03:00",
"assumedAt": "2026-10-15T09:06:00-03:00",
"closedAt": "2026-10-15T11:00:00-03:00",
"customerData": { "nome": "João Silva", "plano": "gold", "clienteDesde": 2019 },
"conversations": [
{
"conversationId": "whatsapp-5511988887766",
"channelType": "WHATSAPP",
"threads": [
{
"threadId": "atendimento-981",
"tags": [{ "reference": "marca", "value": "cyrela", "label": "Cyrela" }],
"messages": [
{ "sentAt": "2026-10-15T09:00:00-03:00", "role": "ai", "text": "Olá! Como posso ajudar?" },
{
"sentAt": "2026-10-15T09:00:40-03:00",
"role": "user",
"text": "Preciso da 2ª via do boleto.",
"authorIdentifier": "529.982.247-25"
},
{
"sentAt": "2026-10-15T09:01:10-03:00",
"role": "user",
"hasMedia": true,
"authorIdentifier": "529.982.247-25"
},
{ "sentAt": "2026-10-15T09:06:30-03:00", "role": "human", "text": "Vou te mandar por e-mail." }
]
}
]
},
{
"conversationId": "email-joao.silva@exemplo.com",
"channelType": "EMAIL",
"threads": [
{
"threadId": "ticket-5521",
"messages": [
{ "sentAt": "2026-10-15T10:30:00-03:00", "role": "human", "text": "Segue o boleto em anexo.", "hasMedia": true },
{
"sentAt": "2026-10-15T11:00:00-03:00",
"role": "user",
"text": "Recebi, obrigado!",
"authorIdentifier": "529.982.247-25"
}
]
}
]
}
]
}
}
bash
curl https://api.hom.notrus.ai/graphql \
-H "Content-Type: application/json" \
-H "apikey: $NOTRUS_API_KEY" \
-d @import-case.json # {"query": "<a mutation acima>", "variables": {"input": {...}}}

Campos

Case (ImportCaseInput)

CampoTipoObrigatórioDescrição
caseIdString
não
Seu número de protocolo. Com ele, o mesmo case é sempre reconhecido; sem ele, a Notrus identifica o case pelo conteúdo.
statusCaseStatus!
sim
Situação do case. Veja os valores na seção Status do case.
resolutionCaseResolution
depende
RESOLVIDO ou NAO_RESOLVIDO. Obrigatório quando o status é ENCERRADO; ignorado nos outros status.
assignedUserNameString
não
Nome do atendente responsável, como está cadastrado na Notrus.
transferredAtString
não
Quando o case foi transferido para atendimento humano, em ISO-8601 com fuso.
assumedAtString
não
Quando um atendente assumiu o case, em ISO-8601 com fuso.
closedAtString
não
Quando o case foi encerrado, em ISO-8601 com fuso. Só vale com o status ENCERRADO.
customerDataJSONObject
não
Dados extras do cliente do case, como um objeto JSON de até 16 KiB, por exemplo { "plano": "gold" }. Veja a seção Cliente.
conversations[ImportCaseConversationInput!]!
sim
As conversas do case, de 1 a 50.

Conversa (ImportCaseConversationInput)

CampoTipoObrigatórioDescrição
conversationIdString!
sim
Identificador da conversa no seu sistema, como o número do WhatsApp ou o e-mail do cliente.
channelTypeString
não
Canal da conversa, como WHATSAPP ou EMAIL.
threads[ImportCaseThreadInput!]!
sim
As threads da conversa neste case, de 1 a 50.

Thread (ImportCaseThreadInput)

CampoTipoObrigatórioDescrição
threadIdString
não
Identificador do atendimento no seu sistema, como o número do ticket. Sem ele, a Notrus identifica a thread pela primeira mensagem.
tags[ImportCaseTagInput!]
não
Tags de classificação da sua organização para a thread, até 100.
messages[ImportCaseMessageInput!]!
sim
As mensagens da thread, em qualquer ordem. O case precisa ter pelo menos uma mensagem user.

Tag (ImportCaseTagInput)

CampoTipoObrigatórioDescrição
referenceString!
sim
Identificador da tag, como marca.
valueString!
sim
Valor da tag, como cyrela.
labelString
não
Nome legível do valor, como Cyrela.

Mensagem (ImportCaseMessageInput)

CampoTipoObrigatórioDescrição
sentAtString!
sim
Data e hora da mensagem em ISO-8601 com fuso, como 2026-10-15T09:00:00-03:00. Precisa ser uma data que existe, entre 2000-01-01 e 24 horas a partir de agora.
roleString!
sim
Autor da mensagem, em texto livre. user (sem diferenciar maiúsculas) marca a mensagem do cliente; use ai para o bot e human para o atendente.
textString
depende
Texto da mensagem. Obrigatório quando não há mídia.
hasMediaBoolean
não
true quando a mensagem tem anexo, imagem ou áudio. Sem texto, a mensagem é gravada como [Mídia Enviada].
authorIdentifierString
depende
Identificador de quem escreveu. Obrigatório nas mensagens user, onde identifica o cliente (CPF, CNPJ, telefone ou e-mail). Nos outros roles é opcional e gravado como veio.

Status do case

ValorSignificado
NOTRUS_ATENDIMENTOEm atendimento pela IA.
ABERTOAguardando um atendente na fila.
ATENDIMENTO_HUMANOCom um atendente.
AGENDADOCom retorno agendado.
ENCERRADOAtendimento finalizado. Exige resolution.
  • resolution só é gravado com o status ENCERRADO. Enviado com outro status, é ignorado e a resposta traz o aviso resolution_ignored.
  • Um case ENCERRADO sem closedAt é fechado no horário da última mensagem.
  • As datas seguem a ordem transferredAt → assumedAt → closedAt. Uma data anterior à que vem antes dela é descartada, com aviso.
  • assignedUserName é procurado entre os usuários da sua organização na Notrus, sem diferenciar maiúsculas, acentos e espaços. Se houver exatamente um com esse nome, ele vira o responsável; se não houver nenhum, ou houver mais de um, o case fica sem responsável e a resposta traz um aviso.

Identificadores e reenvio

  • Com caseId, a Notrus calcula o id do case a partir dele. Sem caseId, o id vem do conteúdo das mensagens.
  • Com threadId, o id da thread vem dele e da conversa. Sem threadId, vem da primeira mensagem da thread.
  • O id de cada mensagem vem da conversa, do horário, do role e do texto. A mesma mensagem enviada de novo não é duplicada, e nada que já foi gravado é apagado.
  • Por isso a chamada pode ser repetida com segurança: se ela falhar ou der timeout, envie as mesmas variáveis de novo. O outcome vem como UNCHANGED quando tudo já estava gravado.
  • Só a thread mais recente de cada conversa fica ativa. Ao importar um case com uma conversa que já tem uma thread mais antiga, a antiga passa a inativa.

Cliente

Todo case importado tem um cliente. Ele é resolvido somente a partir das mensagens com role = user, pelo authorIdentifier, que é obrigatório nessas mensagens. Vale o primeiro identificador válido em ordem de sentAt, em qualquer conversa do case; o cliente é gravado no case e em todas as threads.

  • A importação é recusada com CASE_IMPORT.VALIDATION_FAILED, e nada é gravado, quando o case não tem nenhuma mensagem user, quando uma mensagem user vem sem authorIdentifier ou quando nenhum identificador das mensagens user é válido.
  • O tipo é deduzido: com @ é e-mail; no formato de CNPJ ou CPF, é documento; o resto é telefone. Telefones em formatos diferentes (com ou sem 9, com ou sem +55) são o mesmo cliente.
  • A Notrus guarda só uma versão mascarada do identificador, como ***.***.***-25 ou ***7766. Um CPF ou CNPJ com dígito verificador errado não identifica ninguém: se o case tiver outro identificador válido, a importação segue com o aviso invalid_customer_identifier.
  • Em mensagens com qualquer outro role (ai, human ou um valor livre), o authorIdentifier nunca é tratado como cliente: é gravado como veio, para identificar o autor.
  • customerData guarda dados extras do cliente, como nome ou plano, num objeto JSON. A cada importação, as chaves enviadas substituem as gravadas e as outras continuam, inclusive as de outros cases do mesmo cliente; campos null são ignorados. Sem customerData, nada muda. Os dados saem junto com o cliente em deleteImportedCustomer.

Tags

  • Toda thread importada recebe a tag importacao = true, para separar o que veio do seu sistema do que foi atendido pela Notrus.
  • Uma tag que ainda não existe na sua organização é criada. Cada thread guarda um valor por tag. Tags internas da Notrus não podem ser escritas pela importação; elas são ignoradas, com aviso.
  • channelType vira a tag channel da thread, quando esse valor existe no catálogo da sua organização.

Resposta

O ImportCaseResult descreve o que foi gravado: o case com o código gerado, o cliente e, para cada conversa, as threads com o que entrou de mensagens e tags.

json
{
"data": {
"importCase": {
"outcome": "CREATED",
"case": { "id": "3b5f2c1e-8a47-5d1b-9c2e-6f0a4d7e8b13", "code": "CS-0042", "outcome": "CREATED" },
"customer": { "id": "c76dad57-44f0-45e6-bf23-1ca6e5cc467f", "outcome": "CREATED" },
"conversations": [
{
"conversationId": "whatsapp-5511988887766",
"threads": [
{
"id": "e6abb09d-a659-56fb-8cce-d8cdd3ab13e4",
"outcome": "CREATED",
"active": true,
"traces": { "inserted": 4, "duplicated": 0, "missingFromPayload": 0 },
"tags": { "linked": 2, "replaced": 0, "tagsCreated": 1, "valuesCreated": 1 }
}
]
},
{
"conversationId": "email-joao.silva@exemplo.com",
"threads": [
{
"id": "9e04afc7-69af-5d62-ada9-bb73b80fd323",
"outcome": "CREATED",
"active": true,
"traces": { "inserted": 2, "duplicated": 0, "missingFromPayload": 0 },
"tags": { "linked": 1, "replaced": 0, "tagsCreated": 0, "valuesCreated": 0 }
}
]
}
],
"warnings": []
}
}
}
ValorSignificado
CREATEDAlgo novo foi criado: o case ou pelo menos uma thread.
UPDATEDO case já existia e recebeu mudanças: mensagens, tags, cliente ou campos do case.
UNCHANGEDTudo o que veio já estava gravado.

Em cada thread, traces.inserted conta as mensagens novas, traces.duplicated as que já estavam gravadas e traces.missingFromPayload as gravadas antes que não vieram nesta chamada (elas continuam lá). active indica se a thread é a mais recente da conversa. Uma mensagem é identificada pela conversa, horário, role e texto: se a mesma mensagem já foi gravada em outra thread da conversa, ela conta como duplicated e continua na thread onde entrou primeiro.

As conversas importadas são lidas pelas queries conversations e conversation, como as demais. Um role livre volta como OTHER, com o texto gravado em rawRole (veja Trace).

Avisos

Um problema que não impede a importação não vira erro: a parte afetada é ignorada e o código vai para warnings. Nos códigos marcados com path, o que vem depois dos dois-pontos é a posição do item no input, como conversations[0].threads[1].messages[3].

AvisoSignificado
empty_message_skipped:<path>Mensagem sem texto e sem mídia; foi ignorada.
empty_thread_skipped:<path>Thread em que nenhuma mensagem tinha texto ou mídia; foi ignorada.
empty_conversation_skipped:<path>Conversa em que nenhuma thread tinha mensagens válidas; foi ignorada.
resolution_ignoredresolution enviado com um status diferente de ENCERRADO.
assumed_at_ignored · closed_at_ignoredData anterior à etapa que vem antes dela, ou closedAt com o case aberto.
assigned_user_not_found · assigned_user_ambiguousNenhum usuário (ou mais de um) com o nome em assignedUserName; o case ficou sem responsável.
assigned_user_lookup_unavailableNão foi possível consultar os usuários da organização; o responsável não foi alterado.
invalid_customer_identifierUm authorIdentifier de mensagem user não é um CPF, CNPJ, telefone ou e-mail válido; o cliente veio de outro identificador do case.
multiple_customer_identifiersMais de um cliente nas mensagens user; ficou o primeiro.
duplicated_tag_ignored:<path>Tag repetida na mesma thread, ou com a referência reservada importacao ou channel; ficou a primeira.
reserved_tag:<reference>Tag interna da Notrus; não pode ser escrita pela importação.
inactive_tag:<reference>Tag desativada na sua organização.
invalid_occurrence_value:<reference>Tag de contagem com um valor que não é um número inteiro positivo.
unknown_tag:channel · unknown_tag_value:channelO canal de channelType não existe no catálogo da sua organização; a thread entrou sem a tag channel.
thread_superseded:<threadId>A conversa já tinha uma thread mais antiga; ela passou a inativa.
thread_case_reassigned:<caseId>A thread estava em outro case (o id depois dos dois-pontos) e passou para este. Se o case anterior veio de importação e ficou sem threads, ele foi removido.
customer_divergentA thread já estava ligada a outro cliente; ela continua com o cliente anterior.
case_details_outdated:<caseCode> · tags_outdated:<threadId>A chamada tinha mensagens mais antigas que as já gravadas; status e tags não foram alterados.

Excluir um case importado

A mutation deleteImportedCase apaga um case importado e tudo o que a importação gravou nele. Identifique o case pelo seu caseId ou pelo id da Notrus:

graphql
mutation DeleteImportedCase($input: DeleteImportedCaseInput!) {
deleteImportedCase(input: $input) {
outcome
case { id code }
threads
traces
}
}
json
{
"input": { "caseId": "protocolo-777" }
}
CampoTipoObrigatórioDescrição
caseIdString
depende
O caseId enviado no importCase. Envie este ou id, nunca os dois.
idID
depende
O case.id devolvido pelo importCase. É a única forma de apagar um case importado sem caseId.
json
{
"data": {
"deleteImportedCase": {
"outcome": "DELETED",
"case": { "id": "3b5f2c1e-8a47-5d1b-9c2e-6f0a4d7e8b13", "code": "CS-0042" },
"threads": 2,
"traces": 6
}
}
}
ValorSignificado
DELETEDOs dados foram apagados.
NOT_FOUNDNão havia nada para apagar: não existe, já foi apagado ou é de outra organização.
  • São apagados o case, o histórico de eventos dele, as threads, as mensagens e as tags ligadas a essas threads. threads e traces contam o que foi removido.
  • O cliente continua cadastrado; para apagá-lo, use deleteImportedCustomer. As tags criadas no catálogo da organização também ficam, e o código do case (CS-0042) nunca é reaproveitado: importar o mesmo case de novo gera um código novo.
  • Só dados importados são apagados. Se o case tiver uma thread que não veio da importação, a chamada é recusada com CASE_IMPORT.CASE_HAS_LIVE_THREADS; um case criado pela Notrus é recusado com CASE_IMPORT.CASE_NOT_IMPORTED. Uma thread antiga que ficou inativa quando o case foi importado continua inativa.
  • A exclusão pode ser repetida com segurança: um case que já não existe, ou de outra organização, volta como NOT_FOUND, sem erro.
json
{
"data": {
"deleteImportedCase": {
"outcome": "NOT_FOUND",
"case": null,
"threads": 0,
"traces": 0
}
}
}

Excluir os dados de um cliente (LGPD)

Para atender a um pedido de exclusão de dados, a mutation deleteImportedCustomer apaga o cliente e todos os cases importados dele, de uma vez. Identifique o cliente pelo identificador que você enviou na importação ou pelo id da Notrus:

graphql
mutation DeleteImportedCustomer($input: DeleteImportedCustomerInput!) {
deleteImportedCustomer(input: $input) {
outcome
customer { id }
cases { id code }
threads
traces
}
}
json
{
"input": { "identifier": "529.982.247-25" }
}
CampoTipoObrigatórioDescrição
identifierString
depende
O mesmo valor enviado em authorIdentifier nas mensagens user: CPF, CNPJ, telefone ou e-mail, em qualquer formato aceito pela importação. Envie este ou id, nunca os dois.
idID
depende
O customer.id devolvido pelo importCase.
json
{
"data": {
"deleteImportedCustomer": {
"outcome": "DELETED",
"customer": { "id": "c76dad57-44f0-45e6-bf23-1ca6e5cc467f" },
"cases": [
{ "id": "3b5f2c1e-8a47-5d1b-9c2e-6f0a4d7e8b13", "code": "CS-0042" },
{ "id": "8d2e6a90-1c3b-5f47-a8e2-4b6c9d0e1f23", "code": "CS-0057" }
],
"threads": 3,
"traces": 11
}
}
}
  • São apagados o cliente, com o customerData dele, e, em cada case dele, o mesmo que em deleteImportedCase. cases lista os cases removidos.
  • O identificador é tratado como na importação: telefones em formatos diferentes, CPF com ou sem pontuação e e-mail com maiúsculas encontram o mesmo cliente. Um identificador inválido é recusado com CASE_IMPORT.VALIDATION_FAILED.
  • Se o cliente também foi atendido pela Notrus, ou seja, tem algum case ou thread que não veio da importação, a chamada é recusada com CASE_IMPORT.CUSTOMER_HAS_LIVE_DATA e nada é apagado.
  • Como na exclusão de case, repetir a chamada devolve NOT_FOUND, e as tags do catálogo e os códigos de case não são reaproveitados.

Erros

Quando uma chamada é recusada, nada é gravado nem apagado, data vem nulo e o motivo vem em errors, no formato descrito em Erros. Os códigos possíveis em extensions.code:

extensions.codeHTTPSignificado
GRAPHQL_VALIDATION_FAILED
200
O input não segue o schema: campo obrigatório ausente, tipo errado, valor fora do enum ou campo que não existe. A mensagem diz qual.
BAD_USER_INPUT
200
Formato ou limite violado, como data sem fuso, data que não existe (2026-02-30), data antes de 2000 ou no futuro, texto vazio ou listas grandes demais. A lista de problemas vem em extensions.details.
CASE_IMPORT.VALIDATION_FAILED
200
O case não segue uma regra de importação, como cliente sem identificação ou ENCERRADO sem resolution, ou uma exclusão não traz exatamente um identificador válido. A lista vem em extensions.details.
UNAUTHENTICATED
401
Chave de API ausente ou não reconhecida.
CASE_IMPORT.ORGANIZATION_NOT_FOUND
404
A organização da chave não existe na Notrus.
BAD_REQUEST
400
A requisição não é GraphQL válido: corpo vazio, JSON quebrado ou sem query.
PAYLOAD_TOO_LARGE
413
O corpo passou do limite de 16 MiB. Divida o case em chamadas menores, com o mesmo caseId e threadId e em ordem cronológica: as mensagens se somam sem duplicar.
UNSUPPORTED_MEDIA_TYPE
415
O corpo não foi enviado como application/json.
CASE_IMPORT.CASE_NOT_IMPORTED
200
O case existe, mas não foi criado pela importação; ele não pode ser apagado por esta API.
CASE_IMPORT.CASE_HAS_LIVE_THREADS
200
O case tem threads que não vieram da importação, como um atendimento feito na Notrus depois. Nada foi apagado.
CASE_IMPORT.CUSTOMER_HAS_LIVE_DATA
200
O cliente tem cases ou threads que não vieram da importação. Nada foi apagado.
CASE_IMPORT.FAILED
200
Falha inesperada do servidor. Envie a mesma chamada de novo; ela não duplica nada.

Regras do case

Todos os problemas encontrados voltam juntos em extensions.details:

json
{
"data": null,
"errors": [
{
"message": "The case was rejected; see extensions.details",
"path": ["importCase"],
"extensions": {
"code": "CASE_IMPORT.VALIDATION_FAILED",
"details": [
"resolution is required when status is encerrado",
"conversations[0].threads[0].messages[1] with role user has no authorIdentifier"
]
}
}
]
}

Formato e limites

Cada item de extensions.details traz o caminho do campo e o problema:

json
{
"data": null,
"errors": [
{
"message": "Invalid input; see extensions.details",
"path": ["importCase"],
"extensions": {
"code": "BAD_USER_INPUT",
"details": [
"conversations.0.threads.0.messages.0.sentAt must be an ISO-8601 timestamp with a UTC offset"
]
}
}
]
}

Limites por chamada

ItemLimiteSignificado
conversations50Conversas por case.
threads50Threads por conversa.
messages5.000Mensagens somando todas as threads do case.
tags100Tags por thread.
text32.000Caracteres no texto de uma mensagem.
role64Caracteres em role.
caseId · conversationId · threadId256Caracteres em cada identificador.
body16 MiBTamanho do corpo da requisição.
customerData16 KiBTamanho de customerData, em JSON.
assignedUserName · authorIdentifier · tags[]255Caracteres no responsável, no authorIdentifier e em cada campo de tag.

Uma chamada acima de qualquer limite é recusada e nada é gravado. A mutation também conta nos Limites de uso da API.