Skip to content

API quickstart

From an agent token to a draft notification ready for the lawyer to confirm, with curl and TypeScript against the /v1 API.

Updated:

This guide walks the whole flow against https://www.notificado.co/v1: create the case, add a party, upload the auto admisorio, prepare the notification and request its send. The send itself is always confirmed by a lawyer in the panel: no token can send.

Step Operation Scope
1 POST /v1/create-case cases:write
2 POST /v1/add-party cases:write
3 POST /v1/request-upload, PUT the bytes, POST /v1/confirm-upload cases:write
4 POST /v1/create-notification notifications:write
5 POST /v1/attach-document notifications:write
6 Recipients, with the art. 8 sworn statement the lawyer, in the panel
7 POST /v1/send-notification → awaiting_confirmation notifications:write
8 Confirm the send (confirmSend) the lawyer, in the panel
9 GET /v1/get-notification, GET /v1/get-case notifications:read, cases:read
10 Lists, one page at a time: GET /v1/notification-list notifications:read

0. Token and firm

Issue a token under Account → API (/cuenta/api) with cases:write and notifications:write, and keep it in an environment variable, never in code:

export NOTIFICADO_TOKEN='ult_prod_…'

Several operations take orgId, your firm's id. The same token tells you which it is through the MCP server's whoami tool:

curl -X POST 'https://www.notificado.co/mcp' \
  -H "authorization: Bearer $NOTIFICADO_TOKEN" \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"whoami","arguments":{}}}'

In TypeScript (Bun, Node 18+ or Deno), one helper for every call below. It sends a fresh Idempotency-Key each time (operations that are not creates ignore it); see Idempotent retries for when to reuse one.

const BASE = 'https://www.notificado.co/v1';
const token = process.env.NOTIFICADO_TOKEN;
const orgId = '<orgId>'; // from whoami

async function call(path: string, body: unknown, key = crypto.randomUUID()) {
  const response = await fetch(`${BASE}/${path}`, {
    method: 'POST',
    headers: {
      authorization: `Bearer ${token}`,
      'content-type': 'application/json',
      'idempotency-key': key,
    },
    body: JSON.stringify(body),
  });
  const result = await response.json();
  if (!response.ok) throw new Error(`${result.code}: ${result.cause}. Fix: ${result.fix}`);
  return result;
}

1. Create the case

radicado is the 23-digit number as printed on the auto; dashes and spaces are allowed. If the firm already has it, the answer is X_CASE_RADICADO_TAKEN: use the existing case.

curl -X POST 'https://www.notificado.co/v1/create-case' \
  -H "authorization: Bearer $NOTIFICADO_TOKEN" \
  -H "idempotency-key: $(uuidgen)" \
  -H 'content-type: application/json' \
  -d '{"radicado":"05001310300320260012300","juzgado":"Juzgado 3 Civil del Circuito de Medellín","juzgadoEmail":"j03cctomed@cendoj.ramajudicial.gov.co","ciudad":"Medellín","especialidad":"Civil","claseProceso":"Verbal","cliente":"Inversiones Andinas S.A.S."}'
const legalCase = await call('create-case', {
  radicado: '05001310300320260012300',
  juzgado: 'Juzgado 3 Civil del Circuito de Medellín',
  juzgadoEmail: 'j03cctomed@cendoj.ramajudicial.gov.co',
  ciudad: 'Medellín',
  especialidad: 'Civil',
  claseProceso: 'Verbal',
  cliente: 'Inversiones Andinas S.A.S.',
});

2. Add a party

A party is not a recipient: who is notified, and at which address, is sworn by the lawyer in the panel.

curl -X POST 'https://www.notificado.co/v1/add-party' \
  -H "authorization: Bearer $NOTIFICADO_TOKEN" \
  -H "idempotency-key: $(uuidgen)" \
  -H 'content-type: application/json' \
  -d '{"caseId":"<caseId>","role":"demandado","name":"Comercializadora del Valle S.A.S.","idNumber":"900123456-7"}'
await call('add-party', {
  caseId: legalCase.id,
  role: 'demandado',
  name: 'Comercializadora del Valle S.A.S.',
  idNumber: '900123456-7',
});

3. Upload the document

Three calls, because file bytes never travel inside an API call (1 MB limit): ask for a grant, PUT the bytes to the url it answers (with exactly the contentType it answers, before expiresAt), and confirm. The confirmation re-reads the stored bytes, checks their real type (PDF, PNG or JPEG) and computes their SHA-256.

curl -X POST 'https://www.notificado.co/v1/request-upload' \
  -H "authorization: Bearer $NOTIFICADO_TOKEN" \
  -H "idempotency-key: $(uuidgen)" \
  -H 'content-type: application/json' \
  -d '{"kind":"otro","filename":"auto-admisorio.pdf","contentType":"application/pdf","size":48213}'

# If the answer's url is a path (/_storage/…), prefix https://www.notificado.co
curl -X PUT '<url>' \
  -H 'content-type: application/pdf' \
  --data-binary @auto-admisorio.pdf

curl -X POST 'https://www.notificado.co/v1/confirm-upload' \
  -H "authorization: Bearer $NOTIFICADO_TOKEN" \
  -H 'content-type: application/json' \
  -d '{"key":"<key>","kind":"otro","filename":"auto-admisorio.pdf","contentType":"application/pdf"}'
import { readFile } from 'node:fs/promises';

const bytes = await readFile('auto-admisorio.pdf');
const grant = await call('request-upload', {
  kind: 'otro',
  filename: 'auto-admisorio.pdf',
  contentType: 'application/pdf',
  size: bytes.byteLength,
});
const target = new URL(grant.url, 'https://www.notificado.co');
const put = await fetch(target, {
  method: grant.method,
  headers: { 'content-type': grant.contentType },
  body: bytes,
});
if (!put.ok) throw new Error(`upload refused: ${put.status}`);
const document = await call('confirm-upload', {
  key: grant.key,
  kind: 'otro',
  filename: 'auto-admisorio.pdf',
  contentType: 'application/pdf',
});

4. Prepare the notification

A draft: nothing is sent and no credit is used. providenciaFecha is YYYY-MM-DD.

curl -X POST 'https://www.notificado.co/v1/create-notification' \
  -H "authorization: Bearer $NOTIFICADO_TOKEN" \
  -H "idempotency-key: $(uuidgen)" \
  -H 'content-type: application/json' \
  -d '{"orgId":"<orgId>","caseId":"<caseId>","template":"art8-personal","providencia":"Auto admisorio de la demanda","providenciaFecha":"2026-09-21"}'
const notification = await call('create-notification', {
  orgId,
  caseId: legalCase.id,
  template: 'art8-personal',
  providencia: 'Auto admisorio de la demanda',
  providenciaFecha: '2026-09-21',
});

5. Attach the document

curl -X POST 'https://www.notificado.co/v1/attach-document' \
  -H "authorization: Bearer $NOTIFICADO_TOKEN" \
  -H 'content-type: application/json' \
  -d '{"orgId":"<orgId>","notificationId":"<notificationId>","documentId":"<documentId>","kind":"auto_admisorio"}'
await call('attach-document', {
  orgId,
  notificationId: notification.id,
  documentId: document.id,
  kind: 'auto_admisorio',
});

6. Recipients: the lawyer, in the panel

Adding a recipient is the art. 8 sworn statement (Ley 2213 de 2022) about where the address came from: a statement only a lawyer can make, so no API operation does it. Hand the lawyer the notification's link, /notificaciones/<notificationId>.

7. Request the send

With confirm: true, and only after the lawyer has seen the recipients and attachments. The answer is never a sent notification: it is awaiting_confirmation with the panel link.

curl -X POST 'https://www.notificado.co/v1/send-notification' \
  -H "authorization: Bearer $NOTIFICADO_TOKEN" \
  -H 'content-type: application/json' \
  -d '{"orgId":"<orgId>","notificationId":"<notificationId>","confirm":true}'
const request = await call('send-notification', {
  orgId,
  notificationId: notification.id,
  confirm: true,
});
// { status: 'awaiting_confirmation', confirmUrl, expiresAt }

8. The lawyer confirms

The lawyer opens confirmUrl in the panel and confirms within 24 hours (X_SEND_CONFIRMATION_EXPIRED after that). Only then is one credit per recipient used and the message frozen, timestamped and sent.

9. Read the notification and the case

Single-object reads answer the object itself. When the id does not exist, or belongs to another firm, the answer is 404 X_NOT_FOUND: do not retry with the same id.

curl 'https://www.notificado.co/v1/get-notification?orgId=<orgId>&notificationId=<notificationId>' \
  -H "authorization: Bearer $NOTIFICADO_TOKEN"

curl 'https://www.notificado.co/v1/get-case?orgId=<orgId>&caseId=<caseId>' \
  -H "authorization: Bearer $NOTIFICADO_TOKEN"
async function read(path: string, params: Record<string, string>) {
  const response = await fetch(`${BASE}/${path}?${new URLSearchParams(params)}`, {
    headers: { authorization: `Bearer ${token}` },
  });
  const result = await response.json();
  if (!response.ok) throw new Error(`${result.code}: ${result.cause}. Fix: ${result.fix}`);
  return result;
}

const current = await read('get-notification', { orgId, notificationId: notification.id });
const sameCase = await read('get-case', { orgId, caseId: legalCase.id });

Once the notification is delivered, download the constancia or the evidence package with POST /v1/download-evidence (evidence:read); how to check the package is in The perito package.

10. Lists: one page at a time

Every list pages the same way; the example reads the firm's notifications, 100 at a time. Ask for _first rows (the page size); the answer is an envelope with rows, nextCursor and hasMore. While hasMore is true, ask for the next page by sending that nextCursor as _after, with the same _first and the same filters. On the last page nextCursor is null. Without _first, the list answers bare rows, as before, up to its default size (Limits).

curl 'https://www.notificado.co/v1/notification-list?orgId=<orgId>&_first=100' \
  -H "authorization: Bearer $NOTIFICADO_TOKEN"
# { "rows": [ … ], "nextCursor": "<cursor>", "hasMore": true }

curl 'https://www.notificado.co/v1/notification-list?orgId=<orgId>&_first=100&_after=<cursor>' \
  -H "authorization: Bearer $NOTIFICADO_TOKEN"
const all = [];
let after: string | null = null;
do {
  const page = await read('notification-list', {
    orgId,
    _first: '100',
    ...(after === null ? {} : { _after: after }),
  });
  all.push(...page.rows);
  after = page.hasMore ? page.nextCursor : null;
} while (after !== null);

A _first above the list's maximum is refused with 400 X_INPUT_INVALID; it is never cut down silently. The cursor is opaque and signed: do not build or edit it, and do not use it on another list (X_CURSOR_INVALID). Each list's default and largest page size are in Limits.

Next: Authentication, scopes and limits · Errors · API changelog · every operation in the reference.