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.
https://api.hom.notrus.ai/graphql
mutation ImportCase($input: ImportCaseInput!) {importCase(input: $input) {outcomecase { id code outcome }customer { id outcome }conversations {conversationIdthreads {idoutcomeactivetraces { 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:
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:
{"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"}]}]}]}}
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)
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
caseId | String | não | Seu número de protocolo. Com ele, o mesmo case é sempre reconhecido; sem ele, a Notrus identifica o case pelo conteúdo. |
status | CaseStatus! | sim | Situação do case. Veja os valores na seção Status do case. |
resolution | CaseResolution | depende | RESOLVIDO ou NAO_RESOLVIDO. Obrigatório quando o status é ENCERRADO; ignorado nos outros status. |
assignedUserName | String | não | Nome do atendente responsável, como está cadastrado na Notrus. |
transferredAt | String | não | Quando o case foi transferido para atendimento humano, em ISO-8601 com fuso. |
assumedAt | String | não | Quando um atendente assumiu o case, em ISO-8601 com fuso. |
closedAt | String | não | Quando o case foi encerrado, em ISO-8601 com fuso. Só vale com o status ENCERRADO. |
customerData | JSONObject | 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)
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
conversationId | String! | sim | Identificador da conversa no seu sistema, como o número do WhatsApp ou o e-mail do cliente. |
channelType | String | não | Canal da conversa, como WHATSAPP ou EMAIL. |
threads | [ImportCaseThreadInput!]! | sim | As threads da conversa neste case, de 1 a 50. |
Thread (ImportCaseThreadInput)
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
threadId | String | 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)
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
reference | String! | sim | Identificador da tag, como marca. |
value | String! | sim | Valor da tag, como cyrela. |
label | String | não | Nome legível do valor, como Cyrela. |
Mensagem (ImportCaseMessageInput)
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
sentAt | String! | 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. |
role | String! | 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. |
text | String | depende | Texto da mensagem. Obrigatório quando não há mídia. |
hasMedia | Boolean | não | true quando a mensagem tem anexo, imagem ou áudio. Sem texto, a mensagem é gravada como [Mídia Enviada]. |
authorIdentifier | String | 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
| Valor | Significado |
|---|---|
NOTRUS_ATENDIMENTO | Em atendimento pela IA. |
ABERTO | Aguardando um atendente na fila. |
ATENDIMENTO_HUMANO | Com um atendente. |
AGENDADO | Com retorno agendado. |
ENCERRADO | Atendimento finalizado. Exige resolution. |
resolutionsó é gravado com o statusENCERRADO. Enviado com outro status, é ignorado e a resposta traz o avisoresolution_ignored.- Um case
ENCERRADOsemclosedAté 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. SemcaseId, o id vem do conteúdo das mensagens. - Com
threadId, o id da thread vem dele e da conversa. SemthreadId, vem da primeira mensagem da thread. - O id de cada mensagem vem da conversa, do horário, do
rolee 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
outcomevem comoUNCHANGEDquando 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 mensagemuser, quando uma mensagemuservem semauthorIdentifierou quando nenhum identificador das mensagensuseré 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
***.***.***-25ou***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 avisoinvalid_customer_identifier. - Em mensagens com qualquer outro
role(ai,humanou um valor livre), oauthorIdentifiernunca é tratado como cliente: é gravado como veio, para identificar o autor. customerDataguarda 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; camposnullsão ignorados. SemcustomerData, nada muda. Os dados saem junto com o cliente emdeleteImportedCustomer.
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.
channelTypevira a tagchannelda 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.
{"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": []}}}
| Valor | Significado |
|---|---|
CREATED | Algo novo foi criado: o case ou pelo menos uma thread. |
UPDATED | O case já existia e recebeu mudanças: mensagens, tags, cliente ou campos do case. |
UNCHANGED | Tudo 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].
| Aviso | Significado |
|---|---|
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_ignored | resolution enviado com um status diferente de ENCERRADO. |
assumed_at_ignored · closed_at_ignored | Data anterior à etapa que vem antes dela, ou closedAt com o case aberto. |
assigned_user_not_found · assigned_user_ambiguous | Nenhum usuário (ou mais de um) com o nome em assignedUserName; o case ficou sem responsável. |
assigned_user_lookup_unavailable | Não foi possível consultar os usuários da organização; o responsável não foi alterado. |
invalid_customer_identifier | Um 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_identifiers | Mais 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:channel | O 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_divergent | A 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:
mutation DeleteImportedCase($input: DeleteImportedCaseInput!) {deleteImportedCase(input: $input) {outcomecase { id code }threadstraces}}
{"input": { "caseId": "protocolo-777" }}
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
caseId | String | depende | O caseId enviado no importCase. Envie este ou id, nunca os dois. |
id | ID | depende | O case.id devolvido pelo importCase. É a única forma de apagar um case importado sem caseId. |
{"data": {"deleteImportedCase": {"outcome": "DELETED","case": { "id": "3b5f2c1e-8a47-5d1b-9c2e-6f0a4d7e8b13", "code": "CS-0042" },"threads": 2,"traces": 6}}}
| Valor | Significado |
|---|---|
DELETED | Os dados foram apagados. |
NOT_FOUND | Nã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.
threadsetracescontam 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 comCASE_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.
{"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:
mutation DeleteImportedCustomer($input: DeleteImportedCustomerInput!) {deleteImportedCustomer(input: $input) {outcomecustomer { id }cases { id code }threadstraces}}
{"input": { "identifier": "529.982.247-25" }}
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
identifier | String | 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. |
id | ID | depende | O customer.id devolvido pelo importCase. |
{"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
customerDatadele, e, em cada case dele, o mesmo que emdeleteImportedCase.caseslista 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_DATAe 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.code | HTTP | Significado |
|---|---|---|
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:
{"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:
{"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
| Item | Limite | Significado |
|---|---|---|
conversations | 50 | Conversas por case. |
threads | 50 | Threads por conversa. |
messages | 5.000 | Mensagens somando todas as threads do case. |
tags | 100 | Tags por thread. |
text | 32.000 | Caracteres no texto de uma mensagem. |
role | 64 | Caracteres em role. |
caseId · conversationId · threadId | 256 | Caracteres em cada identificador. |
body | 16 MiB | Tamanho do corpo da requisição. |
customerData | 16 KiB | Tamanho de customerData, em JSON. |
assignedUserName · authorIdentifier · tags[] | 255 | Caracteres 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.