---
title: "Create a business event and post its journal entry"
method: POST
path: "/companies/{companyId}/business-events"
tags: ["Business Events"]
---

# Create a business event and post its journal entry

`POST /companies/{companyId}/business-events`

## Request

- Base URL: `https://api.medianfi.com/api/v1`
- URL: `https://api.medianfi.com/api/v1/companies/{companyId}/business-events`
- Auth: HTTP bearer

Strict by default (#1705/#1790/#1815): a request that cannot produce a journal entry — unknown event type, unresolvable accounts, or a resolve-time posting blocker (closed or missing period, filing-locked period, locked/non-postable account, out-of-domain date) — returns 422 naming the blocker, before any row is written. Pass allowWithoutJournalEntry: true to save the event with jeGenerationSkipped instead and retry-post after fixing the blocker. A client idempotencyKey identifies one create operation within this company. An identical completed request returns its existing event with 200 and idempotentReplay: true; this never posts again. A changed request or unfinished/uncertain create returns 409. Safely skipped stored outcomes remain visible on replay. A keyed posting or schedule failure returns 409 even on its first response and stays held for review because its write may already have committed. Exception (#2529): when a vendor bill's journal entry posts but its Payables bill cannot be written, the entry is voided and the outcome is known, so the create completes with 201 and jeGenerationError naming the reason; the event stays listed and can be retried or deleted. Vendor bills (#2529) also return 422 field errors for parameters.apItemId (must be a UUID), parameters.dueDate (a real YYYY-MM-DD date, not before the bill date) and parameters.createNewApItem (boolean; not together with apItemId).

## Request body

- CreateBusinessEventRequest: Business rules (supported event type, resolvable accounts, postable period/account) are enforced by the controller and the resolve-time posting guard, not this schema. Unknown extra keys are allowed.
  - `idempotencyKey` string: Optional trimmed client operation key, unique per company. Retain it across retries; use a new key for a new intentional operation. Keyless legacy creates remain separate. Customer creates compare canonical request values (object key order ignored) and bind the authorized primary entity; unfinished creates require review and cannot be retried as new postings.
  - `eventType` string: Generator key, e.g. adjusting_entry. Defaults to "custom" when absent.
  - `title` string
  - `description` string, nullable
  - `amountCents` integer
  - `eventDate` string: ISO date (YYYY-MM-DD). Derived from parameters or today when absent.
  - `parameters` object: Generator parameters (e.g. debitAccount, creditAccount, amount).
  - `allowWithoutJournalEntry` boolean: Opt into the save-now/fix-later flow — the event saves with metadata.jeGenerationSkipped naming the blocker instead of the request failing with 422.
  - `entityId` string, uuid

## Response `200`

Completed operation replayed without creating or posting again; a safely skipped outcome remains visible

- object
  - `success` boolean
  - `data` BusinessEventWire: A business event with its generated preview lines and any posted journal entries. jeGenerationSkipped/jeGenerationError explain a null journalEntryId — skipped means a known, named blocker (or metadata-only event); error means an unexpected posting failure.
    - `idempotentReplay` boolean: True when a completed create operation was returned without creating or posting again. Posting failures/skips retain their own fields.
    - `id` string, uuid
    - `customerId` string, uuid: The company id (wire name kept for accountant-app compatibility).
    - `eventType` string
    - `status` string
    - `parameters` object
    - `journalEntryId` string, uuid, nullable
    - `generatedEntries` object[]
      - `account` string
      - `accountNumber` string, nullable
      - `debit` number
      - `credit` number
      - `memo` string
    - `generatedJEs` object[]
    - `journalEntryState` 'posted' | 'not_posted' | 'void' | 'deleted' | 'missing' | 'mixed': Consolidated state of every journal entry owned by this event.
    - `journalEntryStateCounts` object
      - `posted` integer, required
      - `void` integer, required
      - `deleted` integer, required
      - `missing` integer, required
    - `jeGenerationSkipped` string: Why journal-entry generation was skipped (named blocker, unresolved accounts, or metadata-only event).
    - `jeGenerationError` string: Unexpected posting failure recorded by the residual safety net (also captured to Sentry).
    - `payablesApItemId` string, uuid: #2529: the Payables (AP) item a posted vendor bill created or linked; its initial journal entry is this event's journal entry.
    - `unresolvedAccounts` string[]
    - `createdAt` string, date-time
    - `createdBy` string, nullable

## Other responses

- `201`: Event created (journal entry posted, or explicitly skipped via opt-in)
- `401`: Authentication required
- `403`: Insufficient permissions or not a company member
- `409`: Same key with a different request, or creation still in progress/needs review; no new posting. Also STRIPE_CHARGE_ALREADY_BOOKED (#2424): an API-key caller booked Stripe revenue by hand (the entry credits revenue or debits Stripe Clearing) naming a ch_/pi_/txn_ id that resolves to one of this company's Stripe balance transactions the sync has posted, holds, or will post. Nothing is written; signed-in people are not subject to this check. #2529 vendor bills (vendor_bill / accounts_payable_bill). These 409s are returned while planning, before any row is written: AP_ITEM_SELECTION_REQUIRED — an open, unposted Payables bill already matches this vendor, amount and currency; extensions.candidates lists { id, vendor, amountCents, dueDate, issueDate, billNumber } (and candidateApItemIds). Resend with parameters.apItemId set to the chosen id (and that bill's billDate/dueDate), or with parameters.createNewApItem: true to record a separate bill; that override is audited on the new AP item. AP_ITEM_POSTING_CONFLICT — the selected bill differs in entity, status, amount, currency, bill date, vendor, expense account or due date (extensions.mismatches). AP_ITEM_ALREADY_POSTED — the selected bill already has a journal entry, source event or document. If the bill changes after planning (a concurrent writer), this is not a 409: the create completes with 201, the journal entry is voided, and jeGenerationError says why.
- `422`: Request failed validation

---

[API](https://skmtc.dev/medianfi/apis/median-api.md) · [All operations](https://skmtc.dev/medianfi/apis/median-api/llms.txt) · [OpenAPI document](https://skmtc.dev/medianfi/apis/median-api/revisions/20090ea32b0d?raw)
