Skip to content

REST, sem SDK ​

O que atravessa o fio. É a forma de quem integra em outra linguagem, a partir da OpenAPI publicada em /openapi.json.

Login e sessão ​

sh
# Policial: a matrícula já traduzida para o e-mail do deployment
curl -X POST https://geohub.exemplo.gov.br/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"123456@pm.br","password":"…","mode":"json"}'
# 200 {"data":{"access_token":"…","refresh_token":"…","expires":900000}}   (expires em ms)

curl "https://geohub.exemplo.gov.br/users/me?fields=id,role" -H 'Authorization: Bearer …'
# 200 {"data":{"id":"<UsuarioId>","role":"<PapelId>"}}

curl -X POST https://geohub.exemplo.gov.br/auth/refresh \
  -H 'Content-Type: application/json' -d '{"refresh_token":"…","mode":"json"}'

curl -X POST https://geohub.exemplo.gov.br/auth/logout \
  -H 'Content-Type: application/json' -d '{"refresh_token":"…","mode":"json"}'

O entrega toda rejeição de login como 401; o que as separa é errors[0].extensions.code: INVALID_CREDENTIALS, INVALID_OTP, USER_SUSPENDED, TOKEN_EXPIRED, INVALID_TOKEN. Erro 5xx ou sem rede é "central indisponível". Renove antes de vencer e, num 401 em qualquer rota, renove uma vez e repita; um 403 não renova. O refresh token é de uso único: duas renovações ao mesmo tempo precisam dividir a mesma chamada.

O não faz login: usa o token estático do usuário de serviço direto como Bearer (ou troca e-mail e senha do usuário de serviço por um token em /auth/login, como o exemplo faz).

Os três cabeçalhos do protocolo ​

cabeçalhoquem mandao que é
Authorization: Bearer …todosquem entrou: o access token da sessão, ou o token estático do serviço. O cookie de sessão do painel também vale
x-solicitacao-id: <uuid v7>obrigatório para integrador; recomendado para o appchave de , cunhada pelo cliente antes da primeira tentativa, uma por comando. Repetir a mesma chave devolve o mesmo desfecho com repetido: true. GET não leva
x-autoria: {"tipo":"integrador","id":"…"}opcionalem nome de quem se age. É conferido contra a sessão, nunca obedecido: divergência é 403 AUTORIA_FORJADA

Sem x-autoria, a é a da sessão: mandá-lo serve só para a conferência. O x-solicitacao-id tem de ser um UUID v7 (no npm, a função v7 do pacote uuid); um v4 é 400 SOLICITACAO_INVALIDA.

Toda rota de plugin responde 200 { "dados": …, "repetido"?: true } em sucesso e { "erro": { "codigo", "mensagem"?, "contexto"?, "faltando"?, "detalhes"? } } em erro. Decida sempre pelo codigo. Rotas de ficam em /plugins/…; verbo sem mapeamento de rota vira POST /plugins/<plugin>/<verbo>.

ts
/**
 * O `fetch` autenticado da REST gerada — o que o integrador em outra
 * linguagem escreveria à mão a partir da OpenAPI.
 *
 * Os três cabeçalhos são o protocolo inteiro: o token diz quem entrou, o
 * `x-autoria` diz em nome de quem se age (e vai para ser **conferido**,
 * nunca para mandar) e o `x-solicitacao-id` é a chave de idempotência. O
 * corpo é o argumento do verbo, e nada mais.
 */
async requisitar(
  metodo: 'GET' | 'PATCH' | 'POST',
  caminho: string,
  opcoes: { corpo?: unknown; solicitacaoId?: string } = {},
): Promise<Response> {
  const temCorpo = opcoes.corpo !== undefined;
  return this.buscar(`${this.plugins}${caminho}`, {
    ...(temCorpo ? { body: JSON.stringify(opcoes.corpo) } : {}),
    headers: {
      Authorization: `Bearer ${this.token}`,
      ...(temCorpo ? { 'Content-Type': 'application/json' } : {}),
      ...(metodo === 'GET'
        ? {}
        : { 'x-autoria': JSON.stringify(this.autoria) }),
      ...(opcoes.solicitacaoId === undefined
        ? {}
        : { 'x-solicitacao-id': opcoes.solicitacaoId }),
    },
    method: metodo,
  });
}
js
/**
 * O `fetch` autenticado da REST gerada — o que o integrador em outra
 * linguagem escreveria à mão a partir da OpenAPI.
 *
 * Os três cabeçalhos são o protocolo inteiro: o token diz quem entrou, o
 * `x-autoria` diz em nome de quem se age (e vai para ser **conferido**,
 * nunca para mandar) e o `x-solicitacao-id` é a chave de idempotência. O
 * corpo é o argumento do verbo, e nada mais.
 */
async requisitar(metodo, caminho, opcoes = {}) {
  const temCorpo = opcoes.corpo !== undefined;
  return this.buscar(`${this.plugins}${caminho}`, {
    ...(temCorpo ? { body: JSON.stringify(opcoes.corpo) } : {}),
    headers: {
      Authorization: `Bearer ${this.token}`,
      ...(temCorpo ? { 'Content-Type': 'application/json' } : {}),
      ...(metodo === 'GET'
        ? {}
        : { 'x-autoria': JSON.stringify(this.autoria) }),
      ...(opcoes.solicitacaoId === undefined
        ? {}
        : { 'x-solicitacao-id': opcoes.solicitacaoId }),
    },
    method: metodo,
  });
}

Um comando completo ​

sh
curl -X POST "https://geohub.exemplo.gov.br/plugins/ocorrencias/$OID/despachos" \
  -H "Authorization: Bearer $T" \
  -H "Content-Type: application/json" \
  -H "x-solicitacao-id: 0199c0a1-7b2e-7c3d-8e4f-5a6b7c8d9e0f" \
  -H 'x-autoria: {"tipo":"integrador","id":"'$INTEGRADOR_ID'"}' \
  -d '{"recurso":{"tipo":"guarnicao","guarnicaoId":"…","viatura":"RP-1207","composicao":[],"identificador":"RP-1207"}}'

As respostas que não são desfecho:

409 {"erro":{"codigo":"SOLICITACAO_EM_ANDAMENTO"}}  + Retry-After: 2   → espere e repita a MESMA chave
422 {"erro":{"codigo":"SOLICITACAO_REUSADA"}}                          → defeito seu: uma chave por comando
400 {"erro":{"codigo":"SOLICITACAO_AUSENTE"}}                          → integrador sem x-solicitacao-id

E as que são:

409 {"erro":{"codigo":"referencia-externa-ja-registrada","contexto":{"ocorrenciaId":"…"}}}
422 {"erro":{"codigo":"viatura-sem-guarnicao-disponivel"}}
409 {"erro":{"codigo":"transicao-invalida","mensagem":"…"}}
403 {"erro":{"codigo":"SEM_CAPACIDADE","faltando":["ocorrencia.atender"]}}
403 {"erro":{"codigo":"GUARDA_DE_DOMINIO","mensagem":"O despacho é de outra guarnição."}}

O exemplo do integrador escreve exatamente essa tradução, e é por ela que o roteiro roda pela REST sem um caso editado:

ts
/** O envelope de resposta de toda rota do plugin. */
interface RespostaDoPlugin {
  readonly dados?: unknown;
  readonly erro?: {
    readonly codigo?: string;
    readonly contexto?: Readonly<Record<string, unknown>>;
    readonly mensagem?: string;
  };
}

/**
 * Os códigos que são recusa de **protocolo** — defeito do integrador, não
 * regra de negócio. Eles viram exceção; o resto vira `Desfecho`.
 */
const RECUSAS_DE_PROTOCOLO = new Set([
  'AUTORIA_FORJADA',
  'CORPO_INVALIDO',
  'ERRO_INTERNO',
  'NAO_AUTENTICADO',
  'SEM_CAPACIDADE',
  'SOLICITACAO_AUSENTE',
  'SOLICITACAO_EM_ANDAMENTO',
  'SOLICITACAO_INVALIDA',
  'SOLICITACAO_REUSADA',
  'VERBO_DESCONHECIDO',
]);

export { ErroDeProtocolo } from './erro-de-protocolo.js';

/** O erro do fio: texto até ser lido pelo contrato. */
interface ErroNoFio {
  readonly codigo: string;
  readonly contexto?: Readonly<Record<string, unknown>>;
  readonly detalhe: string;
}

/**
 * Traduz a resposta HTTP em `Desfecho`, com `ler` dando forma ao `dados` e
 * `lerFalha` dando forma à falha pelo contrato do plugin. Um código que o
 * contrato não conhece não vira desfecho: é o contrato que divergiu, e isso
 * é recusa de protocolo, como as outras.
 *
 * `GUARDA_DE_DOMINIO` fica de fora das recusas de protocolo de propósito:
 * ela é um `403` que diz "esta guarnição não é sua", e o integrador a trata
 * como resposta — não como bug.
 */
async function desfechoDe<T, F extends { readonly codigo: string }>(
  resposta: Response,
  ler: (dados: unknown) => T,
  lerFalha: (erro: ErroNoFio) => F | undefined,
): Promise<Desfecho<T, F>> {
  const corpo = (await resposta.json()) as RespostaDoPlugin;
  if (resposta.ok) return { aceito: true, valor: ler(corpo.dados) };
  const codigo = corpo.erro?.codigo ?? 'ERRO_DESCONHECIDO';
  const mensagem = corpo.erro?.mensagem ?? `HTTP ${resposta.status}`;
  if (RECUSAS_DE_PROTOCOLO.has(codigo)) {
    const retryAfter = Number(resposta.headers.get('Retry-After') ?? '');
    throw new ErroDeProtocolo(
      resposta.status,
      codigo,
      mensagem,
      Number.isInteger(retryAfter) && retryAfter > 0 ? retryAfter : undefined,
    );
  }
  const falha = lerFalha({
    codigo,
    ...(corpo.erro?.contexto === undefined
      ? {}
      : { contexto: corpo.erro.contexto }),
    detalhe: mensagem,
  });
  if (falha === undefined) {
    throw new ErroDeProtocolo(
      resposta.status,
      codigo,
      CODIGOS_CONHECIDOS.has(codigo)
        ? `a falha ${codigo} veio sem o contexto que o contrato promete: ${mensagem}`
        : `código fora do contrato do plugin: ${mensagem}`,
    );
  }
  return { aceito: false, ...falha };
}

const CODIGOS_CONHECIDOS = new Set<string>([
  ...Object.values(CODIGO_DE_FALHA_DE_JORNADA),
  ...Object.values(CODIGO_DE_FALHA_DE_OCORRENCIA),
]);

/** A falha de jornada: todo código do contrato se explica sozinho. */
const falhaDeJornada = (erro: ErroNoFio): FalhaDeJornada | undefined => {
  for (const codigo of Object.values(CODIGO_DE_FALHA_DE_JORNADA)) {
    if (codigo === erro.codigo) return { codigo, detalhe: erro.detalhe };
  }
  return undefined;
};

/** A falha de ocorrência: a referência repetida traz o id da que já existe. */
const falhaDeOcorrencia = (erro: ErroNoFio): FalhaDeOcorrencia | undefined => {
  if (
    erro.codigo ===
    CODIGO_DE_FALHA_DE_OCORRENCIA.REFERENCIA_EXTERNA_JA_REGISTRADA
  ) {
    const existente = erro.contexto?.['ocorrenciaId'];
    return typeof existente === 'string'
      ? {
          codigo: erro.codigo,
          contexto: { ocorrenciaId: ocorrenciaId(existente) },
          detalhe: erro.detalhe,
        }
      : undefined;
  }
  for (const codigo of Object.values(CODIGO_DE_FALHA_DE_OCORRENCIA)) {
    if (
      codigo !==
        CODIGO_DE_FALHA_DE_OCORRENCIA.REFERENCIA_EXTERNA_JA_REGISTRADA &&
      codigo === erro.codigo
    ) {
      return { codigo, detalhe: erro.detalhe };
    }
  }
  return undefined;
};
js
import {
  CODIGO_DE_FALHA_DE_JORNADA,
  guarnicaoId,
  jornadaId,
  selarGuarnicaoElegivel,
  usuarioId,
  viaturaId,
} from '@geocontrol/geohub-sdk/jornada';
import {
  CODIGO_DE_FALHA_DE_OCORRENCIA,
  despachoId,
  ocorrenciaId,
} from '@geocontrol/geohub-sdk/ocorrencia';
import { ErroDeProtocolo } from './erro-de-protocolo.js';
/**
 * Os códigos que são recusa de **protocolo** — defeito do integrador, não
 * regra de negócio. Eles viram exceção; o resto vira `Desfecho`.
 */
const RECUSAS_DE_PROTOCOLO = new Set([
  'AUTORIA_FORJADA',
  'CORPO_INVALIDO',
  'ERRO_INTERNO',
  'NAO_AUTENTICADO',
  'SEM_CAPACIDADE',
  'SOLICITACAO_AUSENTE',
  'SOLICITACAO_EM_ANDAMENTO',
  'SOLICITACAO_INVALIDA',
  'SOLICITACAO_REUSADA',
  'VERBO_DESCONHECIDO',
]);
export { ErroDeProtocolo } from './erro-de-protocolo.js';
/**
 * Traduz a resposta HTTP em `Desfecho`, com `ler` dando forma ao `dados` e
 * `lerFalha` dando forma à falha pelo contrato do plugin. Um código que o
 * contrato não conhece não vira desfecho: é o contrato que divergiu, e isso
 * é recusa de protocolo, como as outras.
 *
 * `GUARDA_DE_DOMINIO` fica de fora das recusas de protocolo de propósito:
 * ela é um `403` que diz "esta guarnição não é sua", e o integrador a trata
 * como resposta — não como bug.
 */
async function desfechoDe(resposta, ler, lerFalha) {
  const corpo = await resposta.json();
  if (resposta.ok) return { aceito: true, valor: ler(corpo.dados) };
  const codigo = corpo.erro?.codigo ?? 'ERRO_DESCONHECIDO';
  const mensagem = corpo.erro?.mensagem ?? `HTTP ${resposta.status}`;
  if (RECUSAS_DE_PROTOCOLO.has(codigo)) {
    const retryAfter = Number(resposta.headers.get('Retry-After') ?? '');
    throw new ErroDeProtocolo(
      resposta.status,
      codigo,
      mensagem,
      Number.isInteger(retryAfter) && retryAfter > 0 ? retryAfter : undefined,
    );
  }
  const falha = lerFalha({
    codigo,
    ...(corpo.erro?.contexto === undefined
      ? {}
      : { contexto: corpo.erro.contexto }),
    detalhe: mensagem,
  });
  if (falha === undefined) {
    throw new ErroDeProtocolo(
      resposta.status,
      codigo,
      CODIGOS_CONHECIDOS.has(codigo)
        ? `a falha ${codigo} veio sem o contexto que o contrato promete: ${mensagem}`
        : `código fora do contrato do plugin: ${mensagem}`,
    );
  }
  return { aceito: false, ...falha };
}
const CODIGOS_CONHECIDOS = new Set([
  ...Object.values(CODIGO_DE_FALHA_DE_JORNADA),
  ...Object.values(CODIGO_DE_FALHA_DE_OCORRENCIA),
]);
/** A falha de jornada: todo código do contrato se explica sozinho. */
const falhaDeJornada = (erro) => {
  for (const codigo of Object.values(CODIGO_DE_FALHA_DE_JORNADA)) {
    if (codigo === erro.codigo) return { codigo, detalhe: erro.detalhe };
  }
  return undefined;
};
/** A falha de ocorrência: a referência repetida traz o id da que já existe. */
const falhaDeOcorrencia = (erro) => {
  if (
    erro.codigo ===
    CODIGO_DE_FALHA_DE_OCORRENCIA.REFERENCIA_EXTERNA_JA_REGISTRADA
  ) {
    const existente = erro.contexto?.['ocorrenciaId'];
    return typeof existente === 'string'
      ? {
          codigo: erro.codigo,
          contexto: { ocorrenciaId: ocorrenciaId(existente) },
          detalhe: erro.detalhe,
        }
      : undefined;
  }
  for (const codigo of Object.values(CODIGO_DE_FALHA_DE_OCORRENCIA)) {
    if (
      codigo !==
        CODIGO_DE_FALHA_DE_OCORRENCIA.REFERENCIA_EXTERNA_JA_REGISTRADA &&
      codigo === erro.codigo
    ) {
      return { codigo, detalhe: erro.detalhe };
    }
  }
  return undefined;
};
// #endregion desfecho-rest
const nada = () => undefined;
const texto = (dados) => String(dados);
/**
 * O selo de elegibilidade é do Geohub: a rota só devolve guarnição
 * `disponivel`, então selar aqui é registrar que foi ele quem conferiu.
 */
const guarnicaoElegivel = (dados) => {
  if (dados === null || dados === undefined) return undefined;
  const lida = dados;
  return selarGuarnicaoElegivel({
    composicao: lida.composicao.map((i) => ({
      posto: i.posto,
      usuario: usuarioId(i.usuario),
    })),
    guarnicaoId: guarnicaoId(lida.guarnicaoId),
    viatura: viaturaId(lida.viatura),
  });
};
const url = (partes, ...valores) =>
  partes.reduce(
    (acumulado, parte, indice) =>
      acumulado +
      parte +
      (indice < valores.length ? encodeURIComponent(valores[indice]) : ''),
    '',
  );
// #region jornada-rest
/** O plugin `jornada` pelos caminhos da OpenAPI. */
export function jornadaPorRest(sessao) {
  return {
    assumirComando: (dados) =>
      sessao.comando(async (solicitacaoId) =>
        desfechoDe(
          await sessao.requisitar(
            'POST',
            url`/guarnicoes/${dados.guarnicaoId}/comando`,
            { corpo: { jornadaId: dados.jornadaId }, solicitacaoId },
          ),
          nada,
          falhaDeJornada,
        ),
      ),
    constituirGuarnicao: (dados) =>
      sessao.comando(async (solicitacaoId) =>
        desfechoDe(
          await sessao.requisitar('POST', '/guarnicoes', {
            corpo: dados,
            solicitacaoId,
          }),
          (id) => guarnicaoId(texto(id)),
          falhaDeJornada,
        ),
      ),
    declararProntidao: (id) =>
      sessao.comando(async (solicitacaoId) =>
        desfechoDe(
          await sessao.requisitar('POST', url`/guarnicoes/${id}/prontidao`, {
            solicitacaoId,
          }),
          nada,
          falhaDeJornada,
        ),
      ),
    // Leitura não leva `solicitacaoId`: consultar não é agir, e repetir um
    // `GET` não muda nada.
    guarnicaoDisponivelNaViatura: async (viatura) => {
      const resposta = await sessao.requisitar(
        'GET',
        url`/viaturas/${viatura}/guarnicao-disponivel`,
      );
      const desfecho = await desfechoDe(
        resposta,
        guarnicaoElegivel,
        falhaDeJornada,
      );
      return desfecho.aceito ? desfecho.valor : undefined;
    },
    iniciarJornada: (dados) =>
      sessao.comando(async (solicitacaoId) =>
        desfechoDe(
          await sessao.requisitar('POST', url`/jornadas/${dados.id}/inicio`, {
            corpo: { posto: dados.posto },
            solicitacaoId,
          }),
          nada,
          falhaDeJornada,
        ),
      ),
    registrarApresentacao: (usuario) =>
      sessao.comando(async (solicitacaoId) =>
        desfechoDe(
          await sessao.requisitar(
            'POST',
            url`/policiais/${usuario}/apresentacao`,
            { solicitacaoId },
          ),
          (id) => jornadaId(texto(id)),
          falhaDeJornada,
        ),
      ),
  };
}
// #endregion jornada-rest
// #region ocorrencia-rest
/** O plugin `ocorrencia` pelos caminhos da OpenAPI. */
export function ocorrenciaPorRest(sessao) {
  return {
    atualizarOcorrencia: (dados) =>
      sessao.comando(async (solicitacaoId) =>
        desfechoDe(
          await sessao.requisitar('PATCH', url`/ocorrencias/${dados.id}`, {
            corpo: { alteracoes: dados.alteracoes },
            solicitacaoId,
          }),
          nada,
          falhaDeOcorrencia,
        ),
      ),
    despachar: (dados) =>
      sessao.comando(async (solicitacaoId) =>
        desfechoDe(
          await sessao.requisitar(
            'POST',
            url`/ocorrencias/${dados.ocorrenciaId}/despachos`,
            {
              corpo: {
                ...(dados.instrucoes === undefined
                  ? {}
                  : { instrucoes: dados.instrucoes }),
                recurso: dados.recurso,
              },
              solicitacaoId,
            },
          ),
          (id) => despachoId(texto(id)),
          falhaDeOcorrencia,
        ),
      ),
    registrarOcorrencia: (dados) =>
      sessao.comando(async (solicitacaoId) =>
        desfechoDe(
          await sessao.requisitar('POST', '/ocorrencias', {
            corpo: dados,
            solicitacaoId,
          }),
          (id) => ocorrenciaId(texto(id)),
          falhaDeOcorrencia,
        ),
      ),
  };
}
// #endregion ocorrencia-rest
ts
/** O plugin `jornada` pelos caminhos da OpenAPI. */
export function jornadaPorRest(sessao: SessaoDoIntegrador): PortaDeJornada {
  return {
    assumirComando: (dados) =>
      sessao.comando(async (solicitacaoId) =>
        desfechoDe(
          await sessao.requisitar(
            'POST',
            url`/guarnicoes/${dados.guarnicaoId}/comando`,
            { corpo: { jornadaId: dados.jornadaId }, solicitacaoId },
          ),
          nada,
          falhaDeJornada,
        ),
      ),

    constituirGuarnicao: (dados) =>
      sessao.comando(async (solicitacaoId) =>
        desfechoDe<GuarnicaoId, FalhaDeJornada>(
          await sessao.requisitar('POST', '/guarnicoes', {
            corpo: dados,
            solicitacaoId,
          }),
          (id) => guarnicaoId(texto(id)),
          falhaDeJornada,
        ),
      ),

    declararProntidao: (id) =>
      sessao.comando(async (solicitacaoId) =>
        desfechoDe(
          await sessao.requisitar('POST', url`/guarnicoes/${id}/prontidao`, {
            solicitacaoId,
          }),
          nada,
          falhaDeJornada,
        ),
      ),

    // Leitura não leva `solicitacaoId`: consultar não é agir, e repetir um
    // `GET` não muda nada.
    guarnicaoDisponivelNaViatura: async (viatura) => {
      const resposta = await sessao.requisitar(
        'GET',
        url`/viaturas/${viatura}/guarnicao-disponivel`,
      );
      const desfecho = await desfechoDe(
        resposta,
        guarnicaoElegivel,
        falhaDeJornada,
      );
      return desfecho.aceito ? desfecho.valor : undefined;
    },

    iniciarJornada: (dados) =>
      sessao.comando(async (solicitacaoId) =>
        desfechoDe(
          await sessao.requisitar('POST', url`/jornadas/${dados.id}/inicio`, {
            corpo: { posto: dados.posto },
            solicitacaoId,
          }),
          nada,
          falhaDeJornada,
        ),
      ),

    registrarApresentacao: (usuario) =>
      sessao.comando(async (solicitacaoId) =>
        desfechoDe<JornadaId, FalhaDeJornada>(
          await sessao.requisitar(
            'POST',
            url`/policiais/${usuario}/apresentacao`,
            { solicitacaoId },
          ),
          (id) => jornadaId(texto(id)),
          falhaDeJornada,
        ),
      ),
  };
}
js
/** O plugin `jornada` pelos caminhos da OpenAPI. */
export function jornadaPorRest(sessao) {
  return {
    assumirComando: (dados) =>
      sessao.comando(async (solicitacaoId) =>
        desfechoDe(
          await sessao.requisitar(
            'POST',
            url`/guarnicoes/${dados.guarnicaoId}/comando`,
            { corpo: { jornadaId: dados.jornadaId }, solicitacaoId },
          ),
          nada,
          falhaDeJornada,
        ),
      ),
    constituirGuarnicao: (dados) =>
      sessao.comando(async (solicitacaoId) =>
        desfechoDe(
          await sessao.requisitar('POST', '/guarnicoes', {
            corpo: dados,
            solicitacaoId,
          }),
          (id) => guarnicaoId(texto(id)),
          falhaDeJornada,
        ),
      ),
    declararProntidao: (id) =>
      sessao.comando(async (solicitacaoId) =>
        desfechoDe(
          await sessao.requisitar('POST', url`/guarnicoes/${id}/prontidao`, {
            solicitacaoId,
          }),
          nada,
          falhaDeJornada,
        ),
      ),
    // Leitura não leva `solicitacaoId`: consultar não é agir, e repetir um
    // `GET` não muda nada.
    guarnicaoDisponivelNaViatura: async (viatura) => {
      const resposta = await sessao.requisitar(
        'GET',
        url`/viaturas/${viatura}/guarnicao-disponivel`,
      );
      const desfecho = await desfechoDe(
        resposta,
        guarnicaoElegivel,
        falhaDeJornada,
      );
      return desfecho.aceito ? desfecho.valor : undefined;
    },
    iniciarJornada: (dados) =>
      sessao.comando(async (solicitacaoId) =>
        desfechoDe(
          await sessao.requisitar('POST', url`/jornadas/${dados.id}/inicio`, {
            corpo: { posto: dados.posto },
            solicitacaoId,
          }),
          nada,
          falhaDeJornada,
        ),
      ),
    registrarApresentacao: (usuario) =>
      sessao.comando(async (solicitacaoId) =>
        desfechoDe(
          await sessao.requisitar(
            'POST',
            url`/policiais/${usuario}/apresentacao`,
            { solicitacaoId },
          ),
          (id) => jornadaId(texto(id)),
          falhaDeJornada,
        ),
      ),
  };
}
ts
/** O plugin `ocorrencia` pelos caminhos da OpenAPI. */
export function ocorrenciaPorRest(
  sessao: SessaoDoIntegrador,
): PortaDeOcorrencia {
  return {
    atualizarOcorrencia: (dados) =>
      sessao.comando(async (solicitacaoId) =>
        desfechoDe(
          await sessao.requisitar('PATCH', url`/ocorrencias/${dados.id}`, {
            corpo: { alteracoes: dados.alteracoes },
            solicitacaoId,
          }),
          nada,
          falhaDeOcorrencia,
        ),
      ),

    despachar: (dados) =>
      sessao.comando(async (solicitacaoId) =>
        desfechoDe<DespachoId, FalhaDeOcorrencia>(
          await sessao.requisitar(
            'POST',
            url`/ocorrencias/${dados.ocorrenciaId}/despachos`,
            {
              corpo: {
                ...(dados.instrucoes === undefined
                  ? {}
                  : { instrucoes: dados.instrucoes }),
                recurso: dados.recurso,
              },
              solicitacaoId,
            },
          ),
          (id) => despachoId(texto(id)),
          falhaDeOcorrencia,
        ),
      ),

    registrarOcorrencia: (dados) =>
      sessao.comando(async (solicitacaoId) =>
        desfechoDe<OcorrenciaId, FalhaDeOcorrencia>(
          await sessao.requisitar('POST', '/ocorrencias', {
            corpo: dados,
            solicitacaoId,
          }),
          (id) => ocorrenciaId(texto(id)),
          falhaDeOcorrencia,
        ),
      ),
  };
}
js
/** O plugin `ocorrencia` pelos caminhos da OpenAPI. */
export function ocorrenciaPorRest(sessao) {
  return {
    atualizarOcorrencia: (dados) =>
      sessao.comando(async (solicitacaoId) =>
        desfechoDe(
          await sessao.requisitar('PATCH', url`/ocorrencias/${dados.id}`, {
            corpo: { alteracoes: dados.alteracoes },
            solicitacaoId,
          }),
          nada,
          falhaDeOcorrencia,
        ),
      ),
    despachar: (dados) =>
      sessao.comando(async (solicitacaoId) =>
        desfechoDe(
          await sessao.requisitar(
            'POST',
            url`/ocorrencias/${dados.ocorrenciaId}/despachos`,
            {
              corpo: {
                ...(dados.instrucoes === undefined
                  ? {}
                  : { instrucoes: dados.instrucoes }),
                recurso: dados.recurso,
              },
              solicitacaoId,
            },
          ),
          (id) => despachoId(texto(id)),
          falhaDeOcorrencia,
        ),
      ),
    registrarOcorrencia: (dados) =>
      sessao.comando(async (solicitacaoId) =>
        desfechoDe(
          await sessao.requisitar('POST', '/ocorrencias', {
            corpo: dados,
            solicitacaoId,
          }),
          (id) => ocorrenciaId(texto(id)),
          falhaDeOcorrencia,
        ),
      ),
  };
}

As rotas de cada estão nas páginas dos casos; a lista completa, na referência e na API.

Eventos ​

Polling por inscrição, o WebSocket do e o do integrador estão em tempo real.

Consultas ​

As consultas de placa (GET /consulta-placa, GET /omitir-alerta) e o registro de pessoa estão em consultar placa e pessoa.