Swagger UI openapi.json openapi.yaml
Guide

BusinessRequests

The asynchronous workflow engine that powers contracts, terminations, and batch invoice emission. A BusinessRequest is a typed action envelope — choose the type, fill the data, and the system handles state transitions, cascading webhooks, and downstream resource updates.

TL;DR

A BusinessRequest is an envelope with a type and a typed payload. Pick the right type, fill the data, and the system drives the state machine for you.
1

Choose the type

new_contract, new_contract_pdf, professional_invoice_emission, or terminate_contract.

2

Fill the data

Each type has a JSON schema. See the schemas section below.

3

POST /business-requests

Returns 201 with the request in draft status, immediately transitioned to open.

4

Poll or wait for callback

Status transitions are async. Use PATCH /update-status from a worker, or POST to /postbacks/n8n/business-requests from n8n.

Architecture

A request is a single record that drives a multi-step workflow. The type column decides which workflow handles the request, and each type has its own side effects and validation.

Your app
business-contabeleza
Side effects + n8n

Side effects cascade: when a request transitions to closed, the linked ProfessionalContract moves to approved; when one of the linked ProfessionalInvoice rows completes, the parent request moves to closed too.

Endpoints

MethodPathPurpose
POST/business-requestsCreate a new BusinessRequest. Selects one of 4 workflows based on attributes.type.
GET/business-requestsList BusinessRequests for the current business.
GET/business-requests/{id}Get a single BusinessRequest.
PATCH/business-requests/{id}Update the mutable data payload. Only allowed in draft/open status.
PATCH/business-requests/{id}/update-statusTransition the request through the status workflow. Cascades to linked resources.
POST/business-requests/{id}/update-statusPOST alias for n8n callers.
PATCH/business-requests/update-statusSame as above, with the id in data.business_request_id.
POST/postbacks/n8n/business-requestsn8n webhook router. Dispatches on data.event; only update_status is wired.
GET/profesionals/{id}/business-requests/historyList all BusinessRequests for a given profesional.
GET/profesionals/{id}/profesional-contracts/activeGet the currently active contract for a profesional.

All endpoints (except the n8n postback) require Authorization: Bearer <jwt> and ilm-business: <id>.

The 4 types

Pick the type that matches your workflow. The system selects the appropriate workflow based on the type field. Use the tabs below to drill into the behavior, side effects, and status flow of each one.

new_contract — start a new digital contract

Kick off a fillable digital contract for a professional. The contract is created in filling status and the profesional or salon can complete and sign it later. Use this when you don't have a signed PDF yet.

Side effect

Creates a ProfessionalContract (status filling) and writes the artifact profesional_contract_id.

Status flow

draftopen once the contract row is persisted.

Linked resource

ProfessionalContract

Schema enforced?

No. The system only reads a few optional fields.

Best for

Onboarding a new profesional into the platform.

new_contract_pdf — register an off-platform signed PDF

Register a contract PDF that was signed outside the platform (physically, with another e-sign tool, etc.). If a contract is already in in_approval, the system attaches the PDF and approves it. Otherwise it creates a new contract and approves it in the same transaction.

Side effect

Reuses or creates a ProfessionalContract, sets its pdf_url, and transitions it to approved.

Status flow

draftopenclosed in a single transaction.

Linked resource

ProfessionalContract

Schema enforced?

No. Only data.pdf_url is read.

Best for

Migrating contracts that were signed off-platform.

profesional_invoice_emission — batch invoice emission

Emit a batch of fiscal invoices in one shot. The action batch-fetches the fiscal entities for each professional, normalizes buyer fields, and creates N ProfessionalInvoice rows. The parent request only closes when every child invoice is done.

Side effect

Creates N ProfessionalInvoice rows. The request closes only after all child invoices complete.

Status flow

draftopen after the invoice rows are persisted.

Linked resource

ProfessionalInvoice (one row per invoice)

Schema enforced?

Yes — strict JSON schema with additionalProperties: false.

Best for

Closing the books for a fiscal month across many professionals.

terminate_contract — request contract termination

Request termination of a contract that is already approved. The status flow depends on whether the signed distrato PDF is available at create time — see "Status flow" below. Use this for a formal end-of-partnership flow that needs approval and dates.

Side effect

Links the termination to the contract record. Writes ProfessionalContract.termination_pdf_url when a distrato PDF is attached. Drives the contract through the appropriate termination path.

Status flow

Two paths:
With data.pdf_url on createdraftclosed (request); contract approvedterminated directly (skips termination_requested), mirrors the new_contract_pdf flow.
Without data.pdf_url on createdraftopen (request); contract approvedtermination_requested. The distrato PDF can be attached later via the status→closed or direct contract status update path.

Linked resource

ProfessionalContract

Schema enforced?

Yes — strict JSON schema with additionalProperties: false.

Document

Optional pdf_url in data on create (PDF path, drives direct-to-terminated); alternatively a top-level termination_pdf_url on the closed request status update or the direct terminated contract status update. All paths write to ProfessionalContract.termination_pdf_url; the column is locked once populated.

Best for

Offboarding a profesional with a formal termination record (and signed distrato proof).

The type column stores the workflow type in lowercase snake_case. The JSON:API payload's attributes.type is what the system reads to pick the right workflow.

Creating a request

The initial status in the request is ignored — a request always starts as draft and is immediately transitioned to open by the system. The system reads attributes.type to pick the right workflow.

Switch between the four tabs to see a working payload for each type.

Example — start a new digital contract

POST /business-requests
Authorization: Bearer <jwt>
ilm-business: 900001
Content-Type: application/vnd.api+json

{
  "data": {
    "type": "business-requests",
    "attributes": {
      "type": "new_contract",
      "data": {
        "profesional_id": "901162"
      }
    }
  }
}

Example — register an off-platform signed PDF

POST /business-requests
Authorization: Bearer <jwt>
ilm-business: 900001
Content-Type: application/vnd.api+json

{
  "data": {
    "type": "business-requests",
    "attributes": {
      "type": "new_contract_pdf",
      "data": {
        "profesional_id": "901162",
        "pdf_url": "https://storage.example.com/contracts/901162-signed.pdf"
      }
    }
  }
}

Example — batch invoice emission

POST /business-requests
Authorization: Bearer <jwt>
ilm-business: 900001
Content-Type: application/vnd.api+json

{
  "data": {
    "type": "business-requests",
    "attributes": {
      "type": "profesional_invoice_emission",
      "data": {
        "fiscal_month": "2026-04",
        "invoices": [
          {
            "profesional_id": "901162",
            "total_value_cents": 1232,
            "description": "Servicos prestados",
            "buyer_fiscal_entity_name": "AME CONCEPT LTDA",
            "buyer_fiscal_entity_tax_number": "48858615000180"
          }
        ]
      }
    }
  }
}

Example — request contract termination (with data.pdf_url, PDF path)

POST /business-requests
Authorization: Bearer <jwt>
ilm-business: 900001
Content-Type: application/vnd.api+json

{
  "data": {
    "type": "business-requests",
    "attributes": {
      "type": "terminate_contract",
      "data": {
        "profesional_contract_id": "554433",
        "partnership_start_date": "2024-01-15",
        "partnership_end_date": "2026-04-30",
        "has_transfer_value": true,
        "transfer_value_cents": 250000,
        "profesional_received_transfer_value": true,
        "has_payment_to_salon": false,
        "salon_received_payment": false,
        "notes": "Encerramento amigavel",
        "pdf_url": "https://public.contabeleza.me/qa/distrato/abc123.pdf"
      }
    }
  }
}

With pdf_url in data: the contract is driven directly approvedterminated and the request goes directly to closed (skipping open and termination_requested), mirroring the new_contract_pdf flow. The URL is written to ProfessionalContract.termination_pdf_url before the contract status transition. If the distrato isn't available yet, omit pdf_url and use the no-pdf path below.

Example — request contract termination (without data.pdf_url, no-PDF path)

POST /business-requests
Authorization: Bearer <jwt>
ilm-business: 900001
Content-Type: application/vnd.api+json

{
  "data": {
    "type": "business-requests",
    "attributes": {
      "type": "terminate_contract",
      "data": {
        "profesional_contract_id": "554433",
        "partnership_start_date": "2024-01-15",
        "partnership_end_date": "2026-04-30",
        "has_transfer_value": true,
        "transfer_value_cents": 250000,
        "profesional_received_transfer_value": true,
        "has_payment_to_salon": false,
        "salon_received_payment": false,
        "notes": "Encerramento amigavel"
      }
    }
  }
}

Without pdf_url in data: the request is opened (draftopen) and the contract transitions to termination_requested. The distrato PDF can be attached later via the status→closed transition or the direct contract status update path.

Response (201 Created)

{
  "data": {
    "id": "42",
    "type": "business-requests",
    "attributes": {
      "type": "profesional_invoice_emission",
      "status": "open",
      "data": { "fiscal_month": "2026-04", "fiscal_date": "2026-04-30", "invoices": [...] },
      "artifacts": {},
      "created_at": "2026-04-15T10:00:00Z",
      "submitted_at": "2026-04-15T10:00:00Z"
    },
    "relationships": {
      "business": { "data": { "type": "businesses", "id": "900001" } }
    }
  }
}

State machine

The status workflow has 7 states and 8 possible transitions. Use the update-status endpoint — never assign status directly.

draft
open
processing
closed

Cancel can happen from draft, open, waiting, processing, or closed:

processing
canceled
FromEventToSide effect
draftopenopenSets submitted_at and submitted_by
openprocessprocessing
processingwaitwaiting
draft, open, waiting, processingcloseclosed
draft, open, waiting, processing, closedcancelcanceledSets canceled_at and canceled_by
waiting, processing, closedcancel_requestcancel_requested
draft, open, waiting, processingerrorerrored

Note: The HTTP update-status endpoint only accepts these 5 values: open, processing, waiting, closed, canceled. cancel_requested and errored are valid statuses but not exposed via the endpoint — they are set by the system internally.

Status transitions — the endpoint

To transition a request from outside, use the update-status endpoint. The id can be in the URL path or in the body.

Path-based (canonical)

PATCH /business-requests/42/update-status
Content-Type: application/vnd.api+json

{
  "data": {
    "id": "42",
    "attributes": { "status": "closed" }
  }
}

Body-based (used by n8n)

PATCH /business-requests/update-status
Content-Type: application/vnd.api+json

{
  "data": {
    "business_request_id": "42",
    "payload": { "status": "closed" }
  }
}

POST alias for n8n HTTP Request nodes

POST /business-requests/42/update-status
Content-Type: application/vnd.api+json

{ "data": { "id": "42", "attributes": { "status": "closed" } } }

All three forms trigger the same status update logic, which cascades the status change to linked resources.

Attaching a distrato on the closed transition

If the signed distrato PDF wasn't available at create time, you can attach it to the linked ProfessionalContract when transitioning the request to closed. Include a top-level termination_pdf_url in the payload alongside status:

PATCH /business-requests/42/update-status
Content-Type: application/vnd.api+json

{
  "data": {
    "id": "42",
    "attributes": {
      "status": "closed",
      "termination_pdf_url": "https://public.contabeleza.me/qa/distrato/abc123.pdf"
    }
  }
}

The same field is accepted on the body-based variant and on the n8n postback. The field is only accepted when the request type is terminate_contract AND status is closed — other combinations ignore it silently. The write happens after the contract transitions to terminated. If the column is already populated, the service errors with 422 TerminationPdfUrlAlreadySet — the URL is locked once written.

The same flow is also reachable from the direct contract status updatePATCH /professional-contracts/{id}/update-status with status: terminated and termination_pdf_url in the body. The contract service writes the column after the contract transitions; the linked request closes as usual. Same lock rules apply.

Create-time data.pdf_url (PDF path)

When the signed distrato is already available at create time, pass data.pdf_url in the terminate_contract payload. The service takes a shorter path: it writes the column, then transitions the contract directly approvedterminated (skipping termination_requested) and closes the request from draft directly to closed (skipping open) — same pattern as the new_contract_pdf flow. The 422 TerminationPdfUrlAlreadySet error applies here too.

Webhooks & callbacks

Webhooks are not auto-fired on commit. The system explicitly fires them at the right moment during the workflow.

n8n postback

POST /postbacks/n8n/business-requests
Content-Type: application/json

{
  "data": {
    "event": "update_status",
    "payload": {
      "id": "42",
      "status": "closed"
    }
  }
}

The postback endpoint dispatches on data.event and only supports update_status. Any other event returns 422 Unknown event.

Response callbacks

Individual request rows can carry a Callback block (stored as JSON) that routes a callback message on every status change. Use this from your client to get notified when the workflow reaches a final state — instead of polling.

JSON schemas per type

Each type validates the attributes.data payload against a JSON schema. The two strict types reject unknown fields (additionalProperties: false) and return 422 with a JSON-schema error message on failure. The two loose types read a small set of well-known fields and ignore the rest.

Strict schema rejects unknown fields
  • profesional_invoice_emission
  • terminate_contract
No schema reads a few fields, ignores the rest
  • new_contract
  • new_contract_pdf

Switch between the four tabs to see the schema table (with field name, type, required, and description) and a working example payload for each type.

new_contract — schema

No JSON schema is enforced. The system reads a few optional fields and ignores the rest. Pass only the data you actually need to bootstrap the contract.

Field Type Required Description
profesional_id string optional The profesional this contract is for. Often omitted when the contract is created in bulk and linked later.
Tip. Because there's no schema, you can pass any extra fields you want — they're stored as-is in attributes.data and available to the downstream n8n workflow.

Example payload

{
  "data": {
    "type": "business-requests",
    "attributes": {
      "type": "new_contract",
      "data": {
        "profesional_id": "901162"
      }
    }
  }
}

new_contract_pdf — schema

No JSON schema is enforced. The system reads two fields: profesional_id (optional) and pdf_url (required). The URL must be publicly accessible so the system can fetch the signed PDF.

Field Type Required Description
profesional_id string optional The profesional this contract is for.
pdf_url string URL required Public URL where the system can fetch the signed PDF. The system downloads it, attaches it to the contract, and transitions the contract to approved. If a contract is already in in_approval, the system reuses it and just attaches this PDF.

Example payload

{
  "data": {
    "type": "business-requests",
    "attributes": {
      "type": "new_contract_pdf",
      "data": {
        "profesional_id": "901162",
        "pdf_url": "https://storage.example.com/contracts/901162-signed.pdf"
      }
    }
  }
}

profesional_invoice_emission — schema

Both the top-level payload and each invoice object are validated strictly (additionalProperties: false). Validation failures return 422 with the offending path and message.

Top-level fields

Field Type Required Description
fiscal_month string required YYYY-MM. Identifies the fiscal month the batch refers to. Constraint: must not be in the future.
fiscal_date string optional YYYY-MM-DD. The exact issue date. Auto-derived if omitted: last day of fiscal_month, or business.local_date if the month is the current one.
invoices array<object> required The batch of invoices to emit. minItems: 1. See the per-invoice schema below.

Per-invoice fields (each element of invoices)

Field Type Required Description
profesional_id string required The seller (profesional) issuing the invoice. Always send as a string, even when numeric. The action does .to_s defensively.
total_value_cents integer >= 1 required Total invoice value in cents.
description string optional Free-text description of the service rendered. Appears on the printed invoice.
buyer_fiscal_entity_name string required Legal name of the buyer (CNPJ holder).
buyer_fiscal_entity_tax_number string required Buyer's CNPJ (digits only, no formatting).
buyer_fiscal_entity_id string optional Existing buyer FiscalEntity id. When present, the system resolves to that buyer and skips lookup by tax number.
seller_fiscal_entity_name string required Legal name of the seller. Auto-populated from the buyer's tax-number mapping when omitted.
seller_fiscal_entity_tax_number string required Seller's CNPJ (digits only). Auto-populated from mapping when omitted.
seller_fiscal_entity_id string optional Existing seller FiscalEntity id. Auto-populated from mapping when omitted.
Gotcha. profesional_id must reference a Profesional with a registered SellerFiscalEntity. If it doesn't, the action returns 422 FiscalEntityNotFound.

Example payload

{
  "data": {
    "type": "business-requests",
    "attributes": {
      "type": "profesional_invoice_emission",
      "data": {
        "fiscal_month": "2026-04",
        "invoices": [
          {
            "profesional_id": "901162",
            "total_value_cents": 1232,
            "description": "Servicos prestados",
            "buyer_fiscal_entity_name": "AME CONCEPT LTDA",
            "buyer_fiscal_entity_tax_number": "48858615000180"
          }
        ]
      }
    }
  }
}

terminate_contract — schema

All fields are validated. Conditional fields only need to be present when the matching boolean is true. The referenced profesional_contract_id must be in approved status — anything else returns 422 InvalidContractStatus.

Field Type Required Description
profesional_contract_id string required The contract being terminated. Must be in approved status.
partnership_start_date string required YYYY-MM-DD. Effective start of the partnership.
partnership_end_date string required YYYY-MM-DD. Effective end of the partnership. Constraint: >= partnership_start_date.
has_transfer_value boolean required Whether a transfer value is paid out as part of the termination. Gates transfer_value_cents and profesional_received_transfer_value.
transfer_value_cents integer >= 1 required if has_transfer_value Transfer value in cents, paid out to the profesional.
profesional_received_transfer_value boolean required if has_transfer_value Whether the profesional has already received the transfer value at the time the request is created.
has_payment_to_salon boolean required Whether the profesional owes a final payment to the salon. Gates payment_to_salon_cents and salon_received_payment.
payment_to_salon_cents integer >= 1 required if has_payment_to_salon Payment owed to the salon in cents.
salon_received_payment boolean required if has_payment_to_salon Whether the salon has already received the payment at the time the request is created.
notes string optional Free-text notes attached to the termination record.
pdf_url string URL optional Public URL of a signed distrato PDF (already uploaded to S3). Mirrored to ProfessionalContract.termination_pdf_url after the request opens. If you don't have the signed PDF at create time, you can attach it later by including termination_pdf_url in the payload when transitioning the request to closed. Once termination_pdf_url is set on the contract, any further write attempt errors with 422 TerminationPdfUrlAlreadySet.

Example payload

{
  "data": {
    "type": "business-requests",
    "attributes": {
      "type": "terminate_contract",
      "data": {
        "profesional_contract_id": "554433",
        "partnership_start_date": "2024-01-15",
        "partnership_end_date": "2026-04-30",
        "has_transfer_value": true,
        "transfer_value_cents": 250000,
        "profesional_received_transfer_value": true,
        "has_payment_to_salon": false,
        "salon_received_payment": false,
        "notes": "Encerramento amigavel",
        "pdf_url": "https://public.contabeleza.me/qa/distrato/abc123.pdf"
      }
    }
  }
}

Cascading effects

When a request transitions status, the system maps the request status to the linked resource's status according to the type-specific rules below.

Switch between the four tabs to see the mapping per type.

new_contractProfessionalContract

The contract is created in filling by the create action, then cascades as the request moves.

BusinessRequest transitionContract status
on createcreated in filling
openin_approval
closedapproved
canceled / cancel_requestedcanceled

new_contract_pdfProfessionalContract

Same target as new_contract, but the PDF case jumps straight to approved on close because the contract is already signed.

BusinessRequest transitionContract status
openin_approval (with pdf_url attached)
closedapproved
canceled / cancel_requestedcanceled

profesional_invoice_emissionProfessionalInvoice (per row)

Cascades apply to each child invoice independently. The parent request only closes when every child invoice is done.

BusinessRequest transitionPer-invoice status
openrequested
closedsuccess
canceled / cancel_requestedcancel

The parent request moves to closed only when all child invoices are done.

terminate_contractProfessionalContract

Drives the contract through the termination flow. Cancel is not supported for this type — termination is a one-way operation. The exact path depends on whether data.pdf_url is provided at create time: with it, the contract is driven directly from approved to terminated; without it, the contract goes through the intermediate termination_requested state. The closed transition also writes the optional termination_pdf_url to the contract (if provided in the status-update payload).

BusinessRequest transitionContract statusSide effect
on create (with data.pdf_url)terminated (driven directly, skips termination_requested)Writes professional_contracts.termination_pdf_url from data.pdf_url, then transitions the contract. The system then closes the request (from draft directly to closed).
on create (without data.pdf_url)termination_requested— (column is written later via the closed transition or the direct contract status update path)
opentermination_requested
closedterminatedIf the payload carries a top-level termination_pdf_url, writes it to the contract's column after the transition.
canceled / cancel_requested— (no cascade)

Gotchas

SymptomCauseWhat to do
Initial status in the request is ignoredAll create actions strip attributes.status and the default transition is draftopenDon't set status in the create payload
Can't PATCH data after processingThe system validates data changes against the current status and rejects changes after the workflow has startedOnly update data while status is draft or open; use update-status for status changes
422 FiscalEntityNotFound on invoice emissionAt least one profesional_id or buyer_fiscal_entity_id has no corresponding FiscalEntityRegister the fiscal entity first, or omit the id and use only tax_number + name
422 InvalidContractStatus on terminateThe referenced contract is not in approved stateOnly terminate approved contracts
Cancel accepted but nothing happened downstreamThe type doesn't have a cascade rule for that source stateCheck the cascading effects table for that type
n8n postback returns 422 Unknown eventThe data.event field is not update_statusOnly update_status is wired; use a direct PATCH for other status changes
profesional_id is a string, not an integerJSON schema types it as string; the system converts it to stringAlways send profesional_id as a string
n8n webhook not firing after a status changeWebhooks are manually triggeredWebhooks are fired at specific points in the workflow; if you're driving status from a custom path, ensure the webhook fires
POST /business-requests returns 422 with no obvious errorSchema validation failure — check the field names match exactly (e.g. fiscal_month not month)Validate against the schemas in the schemas section above
422 TerminationPdfUrlAlreadySet on a terminate_contractThe contract's termination_pdf_url is already populated; it's locked once writtenDon't try to overwrite. If you need to change the doc, attach a new field in a future iteration — for now, the first URL is final
Cannot set pdf_url in data after the request leaves openThe system blocks changes to data once the workflow is past openUse the top-level termination_pdf_url field in the closed status-update payload instead — it writes to the contract's column directly

Ship checklist

  1. ☐ Choose the right type for the workflow
  2. ☐ Fill attributes.data matching the JSON schema for that type
  3. ☐ Don't set status in the create payload (it's ignored)
  4. ☐ POST /business-requests and save the returned id
  5. ☐ Poll the request or set a Callback for async notifications
  6. ☐ Use PATCH /update-status for status transitions from your worker
  7. ☐ If using n8n, POST to /postbacks/n8n/business-requests with event: "update_status"
  8. ☐ Don't PATCH data after the status leaves draft/open
  9. ☐ Handle cascading effects on linked ProfessionalContract and ProfessionalInvoice rows
  10. ☐ For terminate_contract: if the signed distrato PDF is available at create time, include data.pdf_url; otherwise send termination_pdf_url in the closed status-update payload
draft open processing waiting closed errored canceled