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
Choose the type
new_contract, new_contract_pdf, professional_invoice_emission, or terminate_contract.
Fill the data
Each type has a JSON schema. See the schemas section below.
POST /business-requests
Returns 201 with the request in draft status, immediately transitioned to open.
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.
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
| Method | Path | Purpose |
|---|---|---|
| POST | /business-requests | Create a new BusinessRequest. Selects one of 4 workflows based on attributes.type. |
| GET | /business-requests | List 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-status | Transition the request through the status workflow. Cascades to linked resources. |
| POST | /business-requests/{id}/update-status | POST alias for n8n callers. |
| PATCH | /business-requests/update-status | Same as above, with the id in data.business_request_id. |
| POST | /postbacks/n8n/business-requests | n8n webhook router. Dispatches on data.event; only update_status is wired. |
| GET | /profesionals/{id}/business-requests/history | List all BusinessRequests for a given profesional. |
| GET | /profesionals/{id}/profesional-contracts/active | Get 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
draft → open 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
draft → open → closed 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
draft → open 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 create — draft → closed (request); contract approved → terminated directly (skips termination_requested), mirrors the new_contract_pdf flow.
Without data.pdf_url on create — draft → open (request); contract approved → termination_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 approved → terminated 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 (draft → open) 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.
Cancel can happen from draft, open, waiting, processing, or closed:
| From | Event | To | Side effect |
|---|---|---|---|
draft | open | open | Sets submitted_at and submitted_by |
open | process | processing | — |
processing | wait | waiting | — |
draft, open, waiting, processing | close | closed | — |
draft, open, waiting, processing, closed | cancel | canceled | Sets canceled_at and canceled_by |
waiting, processing, closed | cancel_request | cancel_requested | — |
draft, open, waiting, processing | error | errored | — |
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 update — PATCH /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 approved → terminated (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.
- profesional_invoice_emission
- terminate_contract
- 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. |
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. |
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_contract → ProfessionalContract
The contract is created in filling by the create action, then cascades as the request moves.
| BusinessRequest transition | Contract status |
|---|---|
| on create | created in filling |
→ open | → in_approval |
→ closed | → approved |
→ canceled / cancel_requested | → canceled |
new_contract_pdf → ProfessionalContract
Same target as new_contract, but the PDF case jumps straight to approved on close because the contract is already signed.
| BusinessRequest transition | Contract status |
|---|---|
→ open | → in_approval (with pdf_url attached) |
→ closed | → approved |
→ canceled / cancel_requested | → canceled |
profesional_invoice_emission → ProfessionalInvoice (per row)
Cascades apply to each child invoice independently. The parent request only closes when every child invoice is done.
| BusinessRequest transition | Per-invoice status |
|---|---|
→ open | → requested |
→ closed | → success |
→ canceled / cancel_requested | → cancel |
The parent request moves to closed only when all child invoices are done.
terminate_contract → ProfessionalContract
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 transition | Contract status | Side 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) |
→ open | → termination_requested | — |
→ closed | → terminated | If 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
| Symptom | Cause | What to do |
|---|---|---|
Initial status in the request is ignored | All create actions strip attributes.status and the default transition is draft → open | Don't set status in the create payload |
Can't PATCH data after processing | The system validates data changes against the current status and rejects changes after the workflow has started | Only update data while status is draft or open; use update-status for status changes |
422 FiscalEntityNotFound on invoice emission | At least one profesional_id or buyer_fiscal_entity_id has no corresponding FiscalEntity | Register the fiscal entity first, or omit the id and use only tax_number + name |
422 InvalidContractStatus on terminate | The referenced contract is not in approved state | Only terminate approved contracts |
| Cancel accepted but nothing happened downstream | The type doesn't have a cascade rule for that source state | Check the cascading effects table for that type |
n8n postback returns 422 Unknown event | The data.event field is not update_status | Only update_status is wired; use a direct PATCH for other status changes |
profesional_id is a string, not an integer | JSON schema types it as string; the system converts it to string | Always send profesional_id as a string |
| n8n webhook not firing after a status change | Webhooks are manually triggered | Webhooks 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 error | Schema 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_contract | The contract's termination_pdf_url is already populated; it's locked once written | Don'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 open | The system blocks changes to data once the workflow is past open | Use the top-level termination_pdf_url field in the closed status-update payload instead — it writes to the contract's column directly |
Ship checklist
- ☐ Choose the right
typefor the workflow - ☐ Fill
attributes.datamatching the JSON schema for that type - ☐ Don't set
statusin the create payload (it's ignored) - ☐ POST
/business-requestsand save the returned id - ☐ Poll the request or set a
Callbackfor async notifications - ☐ Use PATCH
/update-statusfor status transitions from your worker - ☐ If using n8n, POST to
/postbacks/n8n/business-requestswithevent: "update_status" - ☐ Don't PATCH
dataafter the status leavesdraft/open - ☐ Handle cascading effects on linked
ProfessionalContractandProfessionalInvoicerows - ☐ For
terminate_contract: if the signed distrato PDF is available at create time, includedata.pdf_url; otherwise sendtermination_pdf_urlin theclosedstatus-update payload