DevZap · Guias de integração Voltar ao painel

Agendamentos

Referência campo a campo: /docs#operation/createSchedule.

Um agendamento inverte o fluxo normal da API: em vez de o seu sistema chamar o DevZap a cada mensagem, o DevZap chama o seu sistema no horário combinado, pergunta "o que devo enviar hoje?" e envia a resposta. O DevZap é o relógio; o conteúdo é sempre seu, obtido na hora do disparo por um callback HTTP.

O caso típico: todo dia útil às 11h, enviar a cobrança das parcelas que vencem no dia. A lista muda todo dia, então ela não pode ser cadastrada com antecedência; o callback resolve isso.

Criando um agendamento

POST /api/v1/schedules, com a mesma chave de API dos outros endpoints:

{
  "name": "Contas a pagar - vence hoje",
  "instanceId": "INST-A1B2C3-D4E5F6",
  "daysOfWeek": [1, 2, 3, 4, 5, 6],
  "time": "11:00",
  "timezone": "America/Fortaleza",
  "callbackUrl": "https://erp.example.com/callbacks/devzap",
  "callbackSecret": "troque-por-um-segredo-de-32-ou-mais-chars",
  "enabled": true
}
Campo Obrigatório Descrição
name Sim Nome para você se organizar (1 a 100 caracteres).
instanceId Sim Instância remetente. Precisa pertencer à sua conta (senão 404).
daysOfWeek Sim Dias da semana no padrão ISO 8601: 1=segunda ... 7=domingo. Sem repetição.
time Sim Horário local no formato HH:mm (ex.: 11:00).
timezone Sim Timezone IANA (ex.: America/Fortaleza). O horário vale nesse fuso, não em UTC.
callbackUrl Sim A URL do seu sistema que devolve as mensagens do dia. https obrigatório em produção.
callbackSecret Sim Segredo de 32 a 255 caracteres. Vai no header X-Schedule-Secret de cada chamada, para o seu endpoint autenticar quem chama.
enabled Não true por padrão. false cria pausado.

A resposta traz o agendamento criado. Repare que o segredo não volta: no lugar dele vem apenas o indicador callbackSecretConfigured.

{
  "success": true,
  "data": {
    "id": "SCH-A1B2C3-D4E5F6",
    "instanceId": "INST-A1B2C3-D4E5F6",
    "name": "Contas a pagar - vence hoje",
    "daysOfWeek": [1, 2, 3, 4, 5, 6],
    "time": "11:00",
    "timezone": "America/Fortaleza",
    "callbackUrl": "https://erp.example.com/callbacks/devzap",
    "callbackSecretConfigured": true,
    "enabled": true,
    "createdAt": "2026-08-18T14:00:00.000Z",
    "updatedAt": "2026-08-18T14:00:00.000Z"
  },
  "message": "Agendamento criado com sucesso"
}

Os demais endpoints seguem o CRUD que você espera:

⚠️ Remover a instância também remove os agendamentos dela (e os históricos), pela mesma cascata: um agendamento sem remetente não tem como disparar.

Há um limite de agendamentos por conta (padrão 50, configurável na plataforma pela variável SCHEDULES_MAX_PER_TENANT). O teto é por conta, somando todas as instâncias, e não por instância nem por número: se você mantém 4 automações para cada uma de 5 empresas na mesma conta, são 20 agendamentos contra o mesmo teto. Agendamento pausado (enabled: false) também ocupa vaga; se uma automação foi desligada para valer, prefira o DELETE. Ao exceder, o erro é claro:

{
  "success": false,
  "error": {
    "code": "SCHEDULE_LIMIT_REACHED",
    "message": "Limite de 50 agendamentos por conta atingido"
  }
}

Como o horário é calculado

O relógio verifica os agendamentos a cada minuto. Um agendamento dispara quando hoje (no fuso timezone do agendamento, não em UTC) é um dos daysOfWeek e o horário time local já passou. O servidor roda em UTC, mas toda a conta é feita no seu fuso: 11:00 em America/Fortaleza é 14:00 UTC, e perto da meia-noite o dia da semana considerado é o do seu fuso.

Garantias e semântica que valem a pena conhecer:

O callback de conteúdo

No disparo, o DevZap faz um POST na sua callbackUrl:

POST {callbackUrl}
Content-Type: application/json
X-Schedule-Secret: <o callbackSecret cadastrado>
{
  "scheduleId": "SCH-A1B2C3-D4E5F6",
  "scheduledFor": "2026-08-18T11:00:00-03:00",
  "firedAt": "2026-08-18T11:00:30-03:00"
}

scheduledFor é a janela (o "quando deveria"); firedAt é o instante real do disparo. As duas datas vêm em ISO 8601 com o offset do fuso do agendamento, para o seu sistema saber que "hoje" é o hoje do fuso configurado.

A resposta esperada é 200 com a lista de mensagens do dia:

{
  "messages": [
    {
      "to": "5511123450001",
      "message": "Olá! A parcela 2/4 do contrato 123 vence hoje."
    },
    {
      "to": "5511123450002",
      "message": "Olá! A parcela 1/1 do contrato 456 vence hoje."
    }
  ]
}

Regras de processamento do lado do DevZap:

Regra Comportamento
Timeout 30 segundos por tentativa. Cobre um ERP que precisa subir do zero na primeira requisição do dia.
Retry Erro de rede, resposta 5xx ou timeout ganham 1 nova tentativa (2 no total), 60 segundos depois da primeira. Por isso o seu callback precisa ser somente leitura e idempotente: duas chamadas seguidas devem devolver o mesmo resultado sem efeito colateral.
4xx Falha sem retry (o problema é permanente: secret errado, rota inexistente).
Redirect (3xx) Não é seguido, e conta como falha. O secret nunca é reenviado para um destino escolhido pela resposta.
Lista vazia {"messages": []} encerra a execução como skipped_empty; nada é enviado e não é erro.
Teto Máximo de 100 itens por disparo. O excedente é descartado, mas não em silêncio: messagesRequested conta o que você pediu (não o que coube), o error da execução diz quantos ficaram de fora e o status vira partial. Uma execução truncada nunca aparece como success.
Validação por item to precisa casar ^[0-9]{10,15}$ (só dígitos, com DDI); message não pode ser vazia nem passar de 4096 caracteres. Item inválido é descartado e conta como falha na execução.
Teto por número Cada item conta no teto de envio por hora do número, o mesmo das rotas de envio (protege o número de ban por volume). Passou do teto, o item não sai, o error da execução diz quantos ficaram de fora e o status vira partial (ou failed, se nenhum saiu).
Mesmo número duas vezes Duas mensagens para o mesmo to no mesmo disparo são duas mensagens: as duas saem.
Instância desconectada A execução fecha como failed sem chamar o seu callback e sem chamar números. Se a instância reconectar dentro de 30 minutos da janela, o DevZap tenta a execução de novo sozinho (veja abaixo).

Do seu lado, valide o header X-Schedule-Secret contra o segredo cadastrado e devolva 404 (ou 401) quando não bater; assim, terceiros que descobrirem a URL não conseguem extrair dados.

O envio em si reutiliza o caminho de mensagens da plataforma. Com a fila assíncrona habilitada (MESSAGE_QUEUE_ENABLED), cada item entra na fila com uma chave de idempotência derivada de (agendamento, janela, destinatário): repetições da mesma janela nunca duplicam mensagem, e o retry de envio é o da fila (veja 03; Envio de mensagens). Sem a fila, o envio é direto, um a um, e cada falha individual conta na execução.

Acompanhando execuções

GET /api/v1/schedules/{id}/runs devolve o histórico dos últimos 90 dias (execuções mais antigas são apagadas), mais recente primeiro (parâmetro limit opcional, padrão 20, máximo 100):

{
  "success": true,
  "data": [
    {
      "id": "RUN-1755525630000-a1b2c3",
      "scheduleId": "SCH-A1B2C3-D4E5F6",
      "scheduleName": "Contas a pagar - vence hoje",
      "scheduledFor": "2026-08-18T14:00:00.000Z",
      "firedAt": "2026-08-18T14:00:30.000Z",
      "status": "success",
      "messagesRequested": 2,
      "messagesSent": 2,
      "error": null
    },
    {
      "id": "RUN-1755439230000-d4e5f6",
      "scheduleId": "SCH-A1B2C3-D4E5F6",
      "scheduleName": "Contas a pagar - vence hoje",
      "scheduledFor": "2026-08-17T14:00:00.000Z",
      "firedAt": "2026-08-17T14:00:30.000Z",
      "status": "partial",
      "messagesRequested": 3,
      "messagesSent": 1,
      "error": "2 invalid item(s)"
    },
    {
      "id": "RUN-1755353230000-g7h8i9",
      "scheduleId": "SCH-A1B2C3-D4E5F6",
      "scheduleName": "Contas a pagar - vence hoje",
      "scheduledFor": "2026-08-16T14:00:00.000Z",
      "firedAt": "2026-08-16T14:00:30.000Z",
      "status": "skipped_empty",
      "messagesRequested": 0,
      "messagesSent": 0,
      "error": null
    },
    {
      "id": "RUN-1755267000000-j1k2l3",
      "scheduleId": "SCH-A1B2C3-D4E5F6",
      "scheduleName": "Contas a pagar - vence hoje",
      "scheduledFor": "2026-08-15T14:00:00.000Z",
      "firedAt": null,
      "status": "failed",
      "messagesRequested": 0,
      "messagesSent": 0,
      "error": "missed_window"
    }
  ]
}

Nas execuções, scheduledFor e firedAt vêm em UTC (Z), como todas as datas de resposta da API; o offset local aparece só no corpo do callback.

Status Significado
running Execução em andamento agora (estado transitório; dura segundos).
success Todos os itens recebidos foram enviados.
partial Parte foi enviada; error resume o motivo do restante.
failed Nenhum item foi enviado.
skipped_empty O callback devolveu lista vazia; não havia nada a enviar.

Valores notáveis de error:

ℹ️ Com a fila habilitada, messagesSent conta itens aceitos para envio (a fila garante o retry e a idempotência dali em diante). O acompanhamento fino de entrega continua sendo o dos webhooks de status (veja 04; Webhooks).

Quando o número estava fora do ar

Uma execução que falhou com instance_not_connected é tentada de novo assim que o número reconectar, desde que a janela ainda esteja dentro dos 30 minutos de atraso tolerado (o mesmo limite do missed_window). Passado esse prazo, não há nova tentativa: um lembrete que diz "vence hoje" entregue de madrugada é pior que não entregue.

Três coisas que valem saber:

Fora dessa janela, a recuperação é manual: dispare pelo caminho normal de envio (03; Envio de mensagens).

Todas as automações numa lista só

Se você mantém várias automações no mesmo número, GET /api/v1/schedules/runs devolve as execuções da conta inteira numa chamada só, com o mesmo item, a mesma ordem (mais recente primeiro) e o mesmo parâmetro limit:

{
  "success": true,
  "data": [
    {
      "id": "RUN-1755525630000-a1b2c3",
      "scheduleId": "SCH-A1B2C3-D4E5F6",
      "scheduleName": "Contas a pagar - vence hoje",
      "scheduledFor": "2026-08-18T11:00:00.000Z",
      "firedAt": "2026-08-18T11:00:30.000Z",
      "status": "success",
      "messagesRequested": 2,
      "messagesSent": 2,
      "error": null
    },
    {
      "id": "RUN-1755525630000-b2c3d4",
      "scheduleId": "SCH-B2C3D4-E5F6A1",
      "scheduleName": "Contas a receber",
      "scheduledFor": "2026-08-18T11:00:00.000Z",
      "firedAt": "2026-08-18T11:00:32.000Z",
      "status": "skipped_empty",
      "messagesRequested": 0,
      "messagesSent": 0,
      "error": null
    },
    {
      "id": "RUN-1755518430000-c3d4e5",
      "scheduleId": "SCH-C3D4E5-F6A1B2",
      "scheduleName": "Aprovações pendentes",
      "scheduledFor": "2026-08-18T09:00:00.000Z",
      "firedAt": null,
      "status": "failed",
      "messagesRequested": 0,
      "messagesSent": 0,
      "error": "missed_window"
    }
  ]
}

O scheduleName é lido do agendamento no momento da consulta, não congelado na execução: renomear o agendamento renomeia o histórico inteiro dele. E como execuções morrem junto com o agendamento (cascata), tudo que aparece aqui tem dono: um scheduleId que você não reconhece é um agendamento que existe na sua conta e saiu do seu controle, não um resíduo.

Segurança

Erros

Código Status Quando
SCHEDULE_NOT_FOUND 404 Agendamento inexistente ou de outra conta (indistinguíveis de propósito).
INSTANCE_NOT_FOUND 404 A instanceId informada não existe ou não é sua.
SCHEDULE_LIMIT_REACHED 403 Limite de agendamentos da conta atingido.
SCHEDULE_INVALID_TIMEZONE 400 timezone não é uma timezone IANA.
SCHEDULE_INVALID_CALLBACK_URL 400 URL privada/reservada, ou http em produção.
VALIDATION_ERROR 400 Campo fora do formato (dias, horário, nome, tamanho do secret).
SCHEDULE_ERROR 500 Falha interna; vale retentar a operação de gestão (não o disparo).

A classificação geral de retry dos códigos de contrato está em 05; Erros e retry.


← Todos os guias