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:
GET /api/v1/scheduleslista os agendamentos da conta.GET /api/v1/schedules/{id}mostra um.PATCH /api/v1/schedules/{id}atualiza qualquer campo do POST, parcialmente. Enviar{"enabled": false}pausa (o relógio ignora o agendamento por completo);{"enabled": true}retoma.DELETE /api/v1/schedules/{id}remove o agendamento e o histórico de execuções dele (cascata). Para interromper sem perder o histórico, pause com o PATCH.
⚠️ 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:
- No máximo um disparo por dia por agendamento. A âncora é uma restrição de unicidade no banco de dados sobre (agendamento, janela). Vale mesmo com reinício do serviço no meio, e mesmo com mais de um processo rodando.
- Atraso tolerado: 30 minutos. Se o serviço estiver fora do ar no horário e voltar em até 30
minutos, o disparo acontece atrasado. Passou disso, a janela é registrada no histórico como
failedcomerror: "missed_window"e nada é enviado; o dia seguinte segue normal. - Criar depois do horário não dispara no ato. Um agendamento criado às 11:10 para as 11:00 só passa a valer na próxima ocorrência.
- Pausado é invisível para o relógio. Enquanto
enabledforfalsenão há disparo nem registro de janela perdida. - Editar
timeoutimezonemuda a âncora da janela. Se o novo horário também cair hoje, o dia pode ganhar um segundo disparo (ou ummissed_window). Prefira editar fora do horário de disparo.
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:
missed_window: o serviço esteve fora do ar e o atraso passou de 30 minutos.firedAtficanullporque não houve disparo.interrupted: o processo reiniciou no meio da execução; a mensagem pode ter saído para parte dos destinatários.instance_not_connected: a instância estava desconectada na hora. É o único erro que ganha nova tentativa automática.N item(s) discarded over the 100-item cap: o callback devolveu mais de 100 itens. Divida o envio entre mais agendamentos, ou reduza a lista no seu lado.- Falhas do callback aparecem descritas (
callback failed: HTTP 503 (2 attempts), timeout etc.).
ℹ️ Com a fila habilitada,
messagesSentconta 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:
- Não duplica. Só entram nessa recuperação execuções que não enviaram nada, e a checagem de conexão acontece antes do seu callback ser chamado. Se a execução original não chegou a pedir o conteúdo, não há mensagem repetida possível.
- Seu callback é consultado de novo, na hora da nova tentativa. Se ele recalcula a partir do seu banco, o conteúdo entregue é o atual, não uma cópia do que era às 08:00. Uma conta paga no intervalo simplesmente não aparece.
- A execução continua sendo uma só. Ela não vira duas linhas no histórico: o
scheduledForcontinua sendo o da janela original e ofiredAtpassa a ser o da tentativa que funcionou, então o atraso fica visível.
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
- A
callbackUrlpassa por validação anti-SSRF no cadastro e em cada disparo (com resolução DNS): endereços privados, reservados ou de metadados de nuvem são rejeitados comSCHEDULE_INVALID_CALLBACK_URL. - Em produção a URL precisa ser
https. - O
callbackSecretnunca aparece em resposta da API, em logs ou em mensagens de erro. Para trocá-lo, envie um novo valor no PATCH.
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.