# Recruit API — LLM Context File # Version: 2.3 # Updated: 2026-06-03 # Purpose: Structured reference for AI agents, LLMs, and automation tools. # Full OpenAPI spec: https://encontreumnerd.com.br/docs/api/openapi.json # Changelog v2.2: https://encontreumnerd.com.br/docs/api/changelog-v2.2 (also at docs/modules/OPENCLAW-API-CHANGELOG-v2.2.md) # Plugin manifest: https://encontreumnerd.com.br/.well-known/ai-plugin.json ## Overview Recruit API is the REST API for the Eu Nerd IT Services platform (Brazil's largest IT field service network, 9000+ technicians, 2900 cities). It provides programmatic access to 23 resources across talent management, CRM, ticketing, contracts, invoicing, content, email, and messaging. All endpoints require Bearer token authentication. No public/unauthenticated endpoints exist. ## Connection - Base URL: https://wzcftlekhxlsavsjgfhb.supabase.co/functions/v1/openclaw-api - Auth: Header `Authorization: Bearer {RECRUIT_API_KEY}` - Format: JSON request/response - Pagination: `?limit=50&offset=0` (max 500 per page) - Search: `?search=text` (searches relevant fields per resource) - Rate limit: 200 req/min per API key - Volume limit: 5000 records/hour per API key (exfiltration protection) - All timestamps: ISO 8601 UTC ## Security - All endpoints require valid Bearer token (401 on missing/invalid) - Rate limiting enforced per API key (429 on exceed) - Volume/exfiltration monitoring via api_access_logs table - Service uses server-side service role (bypasses RLS) — API key acts as the auth boundary - No CORS restrictions (designed for server-to-server and agent access) ## Resources and Endpoints (28 resources, 60+ endpoints) ### candidates (Talent Pool) - GET /candidates — List. Filters: etapa, cidade, search (nome, email, cidade). Returns: id, nome, email, telefone, cidade, estado, nivel_senioridade, etapa, status, cnpj, created_at, total_jobs_completed. - GET /candidates/{id} — Detail. Extra fields: cpf, bairro, nivel_ingles, certificacoes, tech_profile, job_categories, total_jobs_all, service_radius_km, schedule_availability, disponibilidade_inicio. - POST /candidates — Create. Required: nome, email, telefone, cidade, estado. Optional: bairro, cep, endereco_linha, curriculo_url, curriculo_mime_type, certificacoes[], nivel_senioridade, nivel_ingles, observacoes. Auto: etapa=triagem. - PATCH /candidates/{id} — Update. Allowed fields: nome, email, telefone, cidade, estado, bairro, etapa (triagem|entrevista|proposta|contratado|arquivado), status (adequado|talvez|nao_adequado), certificacoes, nivel_senioridade, nivel_ingles, observacoes, cnpj, cpf, disponibilidade_inicio, service_radius_km. ### vacancies (Job Postings) - GET /vacancies — List. Filters: status (open|closed|draft), search (title, client_name). Returns: id, title, client_name, location, status, work_mode, contract_type, job_area, created_at, published_at. - GET /vacancies/{id} — Detail. All fields. - POST /vacancies — Create. Required: title. Optional: client_name, description, location, work_mode, contract_type, job_area, requirements, salary_range_min, salary_range_max. Auto: status=draft. - PATCH /vacancies/{id} — Update. Allowed: title, description, client_name, location, status, work_mode, contract_type, job_area, requirements, salary_range_min, salary_range_max, published_at. ### vacancy-applications - GET /vacancy-applications/{vacancy_id} — List applications for a vacancy. Filters: status. Returns candidate data inline (nome, email, telefone, cidade, estado) + form_answers. ### vacancy-requests (Client Vacancy Requests) - GET /vacancy-requests — List. Filters: status (pending|approved|rejected), client_name, search (client_name, job_title). - GET /vacancy-requests/{id} — Detail. All fields. - POST /vacancy-requests — Create. Required: client_name, client_user_id. Optional: job_title, job_description, request_type (default: outsourcing), work_mode, contract_type, positions_count (default: 1), location_city, location_state, start_date, end_date, request_data. Auto: status=pending. - PATCH /vacancy-requests/{id} — Update. Allowed: status, admin_notes, reviewed_by, reviewed_at, converted_vacancy_id, job_title, job_description, work_mode, contract_type, positions_count. Auto: reviewed_at on status=approved. ### allocations (Technician Assignments) - GET /allocations — List. Search: title, client_name. Returns: id, title, candidate_id, client_name, status, start_date, end_date, work_mode, billing_type, billing_value, project_name. - GET /allocations/{id} — Detail. Includes candidate join: nome, email, cidade, estado. ### scheduling (Interview Links) - GET /scheduling — List. Filters: candidate_id, vacancy_id. Returns scheduling_url for each link. - POST /scheduling — Create. Required: candidate_id, created_by. Optional: vacancy_id. Auto: expires in 7 days, token-based URL generated. ### companies (B2B Clients) - GET /companies — List. Search: name, cnpj. Returns: id, name, cnpj, phone, is_active, created_at. - GET /companies/{id} — Detail. All fields. - POST /companies — Create. Required: name. Optional: cnpj, email, phone, city, state, segment. Auto: is_active=true. - PATCH /companies/{id} — Update. Allowed: name, cnpj, email, phone, city, state, segment, is_active, contract_type, monthly_value, status. ### deal-pipelines (CRM Pipelines) - GET /deal-pipelines — List active pipelines with stage configurations. Returns: id, name, slug, stages, default_vibe_tone, is_active, created_at. - GET /deal-pipelines/{id}/stages — [v2.2] Canonical stage catalog for one pipeline. Returns each stage with `key` (slugified), `label`, `position`, `is_terminal`, `aliases[]`, plus top-level `default_stage` (first non-terminal) and `canonical_stage_slugs[]`. Falls back to `deal_pipelines.stages` when catalog table empty (`source` field indicates origin). Use this BEFORE writing any `stage` value to avoid 422. ### deals (CRM Deals) - GET /deals — List. Filters: pipeline_id, stage, search (title, contact_name, contact_email). - GET /deals/lookup?external_id=|source_id=|gmail_thread_id=|contact_email=&pipeline_id= — [v2.3] Direct read without paginated scan (up to 25 hits). `gmail_thread_id` matches `metadata->>gmail_thread_id`. `matched_by` reflects the filter used. - GET /deals/resolve?gmail_thread_id=|contact_email=&pipeline_id= — [v2.3] Resolve a single deal. Returns `status: "resolved"` (with `deal_id`, `confidence:"high"`) when exactly one match, `status: "ambiguous"` (with `candidates[]` + `required_action:"manual_canonicalization"`) when 2+ matches, or `status: "not_found"` when none. Route is matched BEFORE `/{id}` so it never collides with deal ids. - GET /deals/enums/lost-reasons — [v2.3] Canonical list of accepted `lost_reason_code` values with PT-BR labels (includes legacy PT codes `cliente_escolheu_outro` etc. plus new English codes `lost_to_competitor`, `customer_discarded`, `timing_lost`, `no_active_demand`, `price_not_accepted`, `no_response_cycle_exhausted`). - GET /deals/duplicates?contact_email=&min=2 — Guardrail: lists OPEN deals sharing the same email; `is_duplicate:true` when count ≥ min. Use before creating to avoid duplicates. - GET /deals/{id} — Detail. Includes resolved company_name. [v2.2] Supports `?expand=company,activities_count,last_activity_at,stage_history_summary` (comma-separated) to bundle related data in one round-trip. Default payload unchanged when `expand` omitted. - POST /deals — Create. Required: pipeline_id. Optional: company_name (auto-resolves company_id), contact_name, contact_email, contact_phone, description, stage (default: lead), source, external_id. Auto: title from contact/company, stage_entered_at, source=api. [v2.2] When `external_id` provided and a deal with same `metadata->>external_id` exists → 409 `external_id_conflict` (includes `existing_deals[]` + hint). Bypass with `?force=1` to create parallel deal (audited). - POST /deals/canonical-resolution — [v2.2] Elect canonical (master) deal across duplicates. Body: `{ gmail_thread_id?, contact_email?, pipeline_id?, apply?: false, reason? }`. Default is dry-run; with `apply:true` marks duplicates by setting `master_deal_id` on non-canonical rows + writes `canonical_resolution` activities on both sides. At least one of `gmail_thread_id` or `contact_email` is required. - PATCH /deals/{id} — Update. Allowed: stage, value, priority, contact_name, contact_email, contact_phone, notes, owner_id. Auto: stage_entered_at on stage change + deal_activity logged. [v2.2] When stage transition rejected, response now includes `accepted_values[]` (canonical labels) and `canonical_stage_slugs[]`; pass `?force=1` to bypass (audited). Opt-in strict mode: `?stage_strict=1` returns 422 `unknown_stage` (with `allowed_stages[]`) when post-normalization stage is not in the pipeline catalog. Successful PATCH returns header `X-Read-Your-Writes: true`; if stage was normalized, also returns `X-Stage-Normalized: =>`. ### deal-activities (CRM Activity Log) - GET /deal-activities?deal_id={id} — List. Required: deal_id. Filters: type (note|stage_change|email|call|meeting|canonical_resolution). - POST /deals/{id}/activities — Create. Required: type, content. Optional: metadata, source. [v2.2] Idempotent by `metadata.gmail_message_id`, `metadata.idempotency_key`, or header `Idempotency-Key`. Replays return 200 with header `X-Idempotent-Replay: true` (no duplicate row created). ### proposals (Commercial Proposals) - GET /proposals — List. Filters: status (rascunho|ativa|aceita|expirada), client_name, proposal_type, search (title, client_name). Returns: id, title, slug, client_name, proposal_type, status, sent_at, accepted_at, expires_at, created_at. - GET /proposals/{id} — Detail. Returns ALL fields including content, metadata, original_content, signing_token, client_logo_url, source_file_url, vacancy_request_id, created_by, accepted_by, updated_at. - GET /proposals/{id}/tracking — Tracking data: signing_token (for public URL /proposta/{token}), sent_at, accepted_at, accepted_by, expires_at, metadata. - POST /proposals — Create. Required: title, client_name. Optional: proposal_type (default: custom), recipient_name, recipient_email, content ({ sections: [{ title, html, order }] }), metadata (investment, scope, brief), expires_at, slug. Auto: slug generated from client_name if omitted, signing_token (UUID) generated, status=rascunho. - PATCH /proposals/{id} — Update. Allowed: status, title, client_name, content, metadata, expires_at, recipient_name, recipient_email, proposal_type, client_logo_url, source_file_url. Auto: sent_at on status→ativa, accepted_at on status→aceita. - Status lifecycle: rascunho → ativa (sends) → aceita (client accepts via /proposta/{signing_token}) or expirada (auto-expire cron). - Content structure: { sections: [{ title: string, html: string, order: number }] }. Each section is a block of the proposal. - Metadata can hold: investment details, scope fields, brief answers, and any custom key-value pairs for the proposal context. ### service-orders (Field Service) - GET /service-orders — List. Search: code (OS-YYYY-XXXXX), company_name. - GET /service-orders/{id} — Detail. All fields. - POST /service-orders — Create. Required: company_name, description. Optional: category, priority (default: medium), scheduled_date, candidate_id, contact_name, contact_phone, city, state, address. Auto: code=OS-YYYY-XXXXX, status=pending, source=api. - PATCH /service-orders/{id} — Update. Allowed: status, priority, category, candidate_id, scheduled_date, completion_notes, rating, description. Auto: completed_at on status=completed. ### tickets (Helpdesk) - GET /tickets — List. Filters: status (open|in_progress|resolved|closed), search (ticket_number, subject, client_name). - GET /tickets/{id} — Detail with all client_ticket_messages. - POST /tickets — Create. Required: subject, sender_email. Optional: client_name, category, priority (default: medium), sender_name, source (default: api), parsed_data. Auto: ticket_number=TK-YYYYMMDD-XXXX, status=open. - PATCH /tickets/{id} — Update. Allowed: status, priority, category, assigned_to, resolution_notes. Auto: resolved_at on status=resolved|closed, updated_at always. ### leads (CRM Leads) - GET /leads — List. Filters: status, source (diagnostic-form|diagnostic-form-partial|contact-form|website|crm-deal), search (sender_email, sender_name, subject, client_name). Returns parsed_data with score, CNPJ, recommended modules. - GET /leads/{id} — Detail with full parsed_data (score, maturityLevel, recommendations, wizardAnswers). ### contracts (Digital Contracts) - GET /contracts — List. Returns: id, status, candidate_id, employee_id, template_id, sent_at, signed_at, created_at. - GET /contracts/{id} — Detail with contract_signatures[]. ### document-signatures (Document Signing) - GET /document-signatures — List. Filters: status, search (title). - GET /document-signatures/{id} — Detail with document_signature_recipients[]. - GET /document-signatures/{id}/status — Summary: total, signed, pending counts + per-recipient signing_urls for pending. - GET /document-signatures/{id}/download — format=url (1h signed URL) or format=binary (PDF bytes). Prefers signed version. - POST /document-signatures — Create. Required: title, file_url, file_name, recipients[{name, email}]. Optional: message, file_type (pdf|docx), created_by. External URLs downloaded + stored automatically. Signing emails sent. - POST /document-signatures/{id}/resend — Resend. Optional: recipient_id (specific) or all pending. - POST /document-signatures/{id}/cancel — Cancel request + pending recipients. ### invoice-batches (Payment Processing) - GET /invoice-batches — List with stats (total_employees, total_invoice_value, counts by status). Filters: status, search (title). - GET /invoice-batches/{id} — Detail with invoices (+ employee data), transactions, stats. - GET /invoice-batches/{id}/status — Payment transaction summary: pending, processing, completed, failed, returned. - POST /invoice-batches — [v2.2] Create batch. Required: title, reference_month (YYYY-MM-DD). Optional: deadline, notes, status (open|processing|closed|paid|cancelled, default open), created_by (UUID), created_by_email. `created_by` is resolved in this order: explicit UUID → employees.email lookup → auth.users admin lookup → first `master_admin` fallback. Returns 422 `created_by_unresolved` if no UUID can be derived. Returns 201 + created batch. - POST /invoice-batches/{id}/process — Create Transfeera PIX/TED transfers. Features: anti-duplication (idempotency_key), auto-split values >R$15,000, PIX key type auto-detection (CPF/CNPJ/EMAIL/TELEFONE/CHAVE_ALEATORIA), bank account fallback. Invoices transition to processing. - POST /invoice-batches/{id}/close — Close batch, trigger payments. IRREVERSIBLE. Must /process first. ### employees (Team Members) - GET /employees — List. Filters: status, departamento, tipo_contrato, search (nome, email, cargo). Returns: id, nome, email, telefone, cpf, cnpj, cargo, departamento, status, tipo_contrato, data_admissao, data_desligamento, cidade, estado, salario, created_at. - GET /employees/{id} — Detail. All fields including pix_key, banco, agencia, conta, observacoes, foto_url, candidate_id, user_id. - POST /employees — Create. Required: nome, email. Optional: cargo, departamento, tipo_contrato (pj|clt|estagio), data_admissao, telefone, cpf, cnpj, cidade, estado, salario, pix_key, banco, agencia, conta, observacoes, candidate_id, user_id, created_by. Auto: cargo=Colaborador, departamento=Geral, tipo_contrato=pj. ### employee-invoices (Employee Payment) - GET /employee-invoices — List. Filters: status, batch_id, employee_id. - POST /employee-invoices — Create. Required: batch_id, employee_id. Optional: invoice_value, expected_value, description, notes, status (default: requested). Validates batch is open, employee exists, prevents duplicates (409). - GET /employee-invoices/{id} — Detail with employee data (nome, email, cnpj, cargo, departamento). - PATCH /employee-invoices/{id} — Update. Allowed: status, paid_at, notes, final_value, rejection_reason. Auto: paid_at on status=paid. ### invoice-adjustments (Extras & Deductions) - GET /invoice-adjustments?invoice_id={id} — List adjustments for an invoice. Returns data + summary (total_additions, total_deductions, net_applied_to_invoice). - POST /invoice-adjustments — Create. Required: invoice_id, title, type (addition|deduction), value. Optional: apply_to_invoice (default: true), notes. When apply_to_invoice=true, value affects fiscal total. When false, tracked only. - DELETE /invoice-adjustments/{id} — Remove adjustment. ### provider-invoices (Provider Payment) - GET /provider-invoices — List. Filters: status, candidate_id, search (code). - GET /provider-invoices/{id} — Detail with candidate data (nome, email, cnpj). - PATCH /provider-invoices/{id} — Update. Allowed: status, notes, rejection_reason, estimated_payment_date, paid_at. Auto: reviewed_at on approved, paid_at on paid. ### whatsapp (Messaging) - POST /whatsapp — Send template message via Meta Cloud API. Required: template_id (UUID), phone. Optional: variables (key-value). Phone auto-normalized to E.164 (accepts 11999999999, 5511999999999, +5511999999999). Returns: wamid, provider_response. ### articles (Blog/Knowledge Base) - GET /articles — List. Filters: status (draft|published|archived), category, search (title, slug). - GET /articles/{id_or_slug} — Detail. Accepts UUID or slug. All fields including SEO. - POST /articles — Create. Required: title. Optional: slug (auto-generated from title, NFD normalized), content_html, content_markdown, category, status (default: draft), excerpt, tags[], meta_title, meta_description, meta_keywords, featured_image_url, featured_image_alt, author_name, subtitle, reading_time_minutes, word_count, og_title, og_description, og_image, canonical_url, schema_markup, structured_data. Auto: published_at on status=published. 409 on duplicate slug. - PATCH /articles/{id_or_slug} — Update. All POST fields updatable. Auto: published_at on transition to published. - DELETE /articles/{id_or_slug} — Permanently delete. ### metrics (Platform Analytics) - GET /metrics — Aggregated: candidate counts by stage (via get_talent_pool_metrics RPC), total_vacancies, open_tickets, active_service_orders. ### payables (Contas a Pagar) - GET /payables — List. Filters: status, from, to, search. Returns: id, description, amount, due_date, paid_date, paid_amount, status, source, company_id, category_id, notes, created_at. - GET /payables/{id} — Detail with joined category, company, bank_account. - POST /payables — Create. Required: description, amount, due_date. - PATCH /payables/{id} — Update. Allowed: description, amount, due_date, status, paid_date, paid_amount, notes, category_id, bank_account_id. ### receivables (Contas a Receber) - GET /receivables — List. Filters: status, from, to, search. Fields: received_date, received_amount instead of paid_date, paid_amount. - GET /receivables/{id} — Detail with joined category, company, bank_account. - POST /receivables — Create. Required: description, amount, due_date. - PATCH /receivables/{id} — Update. Allowed: description, amount, due_date, status, received_date, received_amount, notes, category_id, bank_account_id. ### cashflow (Fluxo de Caixa) - GET /cashflow — Daily projection. Params: from (default today), to or days (default 90). Returns totals + daily breakdown with balance and balance_adjusted (applies default_rate 3% + variable_cost 5% to unpaid receivables). ### finance-settings - GET /finance-settings — Current settings. - PATCH /finance-settings — Update default_default_rate, default_variable_cost_pct, payout_batch_day, horizon_days. ### recurring-rules (Regras Recorrentes) - GET /recurring-rules — List. Filters: type (payable|receivable), is_active, search. - GET /recurring-rules/{id} — Detail. - POST /recurring-rules — Create. Required: type, description, amount, recurrence, start_date. - PATCH /recurring-rules/{id} — Update. ## Response Format List: `{ "data": [...], "total": 342, "limit": 50, "offset": 0 }` Detail: `{ "data": { ... } }` Error: `{ "error": "Description" }` ## HTTP Status Codes 200 Success | 201 Created | 400 Bad request | 401 Unauthorized | 404 Not found | 405 Method not allowed | 409 Conflict (duplicate slug) | 429 Rate limit | 500 Server error ## Business Rules (Auto-Fill Summary) - candidates: etapa=triagem on create - vacancies: status=draft on create - employees: cargo=Colaborador, departamento=Geral, tipo_contrato=pj on create - service-orders: status=pending, source=api, code=OS-YYYY-XXXXX on create - tickets: status=open, ticket_number=TK-YYYYMMDD-XXXX on create - deals: stage_entered_at on stage change + deal_activity auto-logged - proposals: sent_at on status=ativa - payables/receivables: status=pending, source=manual on create - recurring-rules: is_active=true, next_due_date=start_date on create - cashflow: read-only projection from finance_settings rates ### emails (Transactional Email) - POST /emails — Send email via Resend. Required: to (email), subject, html. Optional: text, cc (string[]), bcc (string[]), from_email (@eunerd.com only), entity_type, entity_id, deal_id. Auto: email logged to email_logs, deal_activity created if deal_id provided. Sender defaults to noreply@eunerd.com. ### categories (Finance Categories) - GET /categories — List. Filters: type (payable|receivable), search. Returns: id, name, type, parent_id, omie_code, is_active. - GET /categories/{id} — Detail. - POST /categories — Create. Required: name, type (payable|receivable). Optional: parent_id, omie_code, is_active. Upserts by omie_code if provided. - PATCH /categories/{id} — Update. Allowed: name, parent_id, omie_code, is_active. ### bank-accounts (Contas Bancárias) - GET /bank-accounts — List. Filters: is_active, search. Returns: id, name, bank_name, agency, account_number, pix_key, is_active, current_balance, omie_account_id, created_at. - GET /bank-accounts/{id} — Detail. ### interview-bookings (Agendamentos de Entrevista) - GET /interview-bookings — List. Filters: candidate_id, vacancy_id, status (pending|confirmed|cancelled|completed), conducted (boolean). - GET /interview-bookings/{id} — Detail with joined slot, candidate, vacancy. - PATCH /interview-bookings/{id} — Update. Allowed: conducted (auto-sets status=completed), conduct_notes, conduct_rating (1-5), status, cancellation_reason, notes. ### google-drive (Google Drive Operations) - POST /google-drive/resolve-folder — Find or create client folder. Required: clientName, parentFolderId. Returns folder info with match type. - POST /google-drive/upload — Upload files to folder. Required: folderId or (clientName + parentFolderId), files[{name, mimeType, content (base64)}]. Optional: metadata, overwrite. Max 10 files, 10MB each. - POST /google-drive/save — Resolve folder + upload in one call. Required: clientName, parentFolderId, files[...]. Optional: metadata, overwrite, dealId, conversationId, generatedBy, tags. ## Response Format List: `{ "data": [...], "total": 342, "limit": 50, "offset": 0 }` Detail: `{ "data": { ... } }` Error: `{ "error": "Description" }` ## HTTP Status Codes 200 Success | 201 Created | 400 Bad request | 401 Unauthorized | 404 Not found | 405 Method not allowed | 409 Conflict (duplicate slug, `external_id_conflict` on POST /deals) | 422 Unprocessable (e.g. `unknown_stage`, `created_by_unresolved`) | 429 Rate limit | 500 Server error ## Response Headers (v2.2) - `X-Read-Your-Writes: true` — Returned by `PATCH /deals/{id}` to confirm the response body reflects the just-persisted state. - `X-Stage-Normalized: =>` — Returned by `PATCH /deals/{id}` when the input `stage` was rewritten by canonicalization. - `X-Idempotent-Replay: true` — Returned by `POST /deals/{id}/activities` when the request matched an existing activity by `gmail_message_id` / `idempotency_key` / `Idempotency-Key` header (no new row was created). ## Changelog v2.2 (2026-06-03) — Highlights for Agents P0 (must update agent prompts/SDKs): - POST /deals now returns 409 `external_id_conflict` when `external_id` collides. Always check before retrying; use `?force=1` only with audit reason. - PATCH /deals/{id} stage rejection responses now expose `accepted_values[]` + `canonical_stage_slugs[]`. Map your enum to those slugs. - POST /deals/{id}/activities is idempotent — set `metadata.gmail_message_id` (preferred for email-driven), `metadata.idempotency_key`, or send header `Idempotency-Key`. - Use the new GET /deal-pipelines/{id}/stages BEFORE writing any stage. Cache result per pipeline_id. P1 (recommended): - Use POST /deals/canonical-resolution (dry-run first) to resolve duplicate clusters by gmail_thread_id/contact_email. Apply only after human/agent review. - Adopt PATCH /deals/{id}?stage_strict=1 in agent flows that must never silently accept unknown stages. - Use GET /deals/{id}?expand=company,activities_count,last_activity_at,stage_history_summary to cut N+1 reads. - Use POST /invoice-batches to create batches programmatically (no more UI-only requirement); pass `created_by_email` if you don't have a UUID. ## Business Rules (Auto-Fill Summary) - candidates: etapa=triagem on create - vacancies: status=draft on create - employees: cargo=Colaborador, departamento=Geral, tipo_contrato=pj on create - service-orders: status=pending, source=api, code=OS-YYYY-XXXXX on create - tickets: status=open, ticket_number=TK-YYYYMMDD-XXXX on create - deals: stage_entered_at on stage change + deal_activity auto-logged - proposals: sent_at on status=ativa - payables/receivables: status=pending, source=manual on create - recurring-rules: is_active=true, next_due_date=start_date on create - cashflow: read-only projection from finance_settings rates ## Unknown Resource Response Requesting an unknown resource returns 404 with `available_resources` array listing all 28 valid resource names plus usage examples.