---
title: "Create a business event for an assigned customer"
method: POST
path: "/accountant/companies/{customerId}/business-events"
tags: ["Business Events"]
---

# Create a business event for an assigned customer

`POST /accountant/companies/{customerId}/business-events`

## Request

- Base URL: `https://api.medianfi.com/api/v1`
- URL: `https://api.medianfi.com/api/v1/accountant/companies/{customerId}/business-events`
- Auth: one of:
  - HTTP bearer
  - API key in header `X-API-Key`

Same strict-by-default contract as the customer endpoint: unresolvable accounts and resolve-time posting blockers (closed/missing period, locked account, out-of-domain date) return 422 before any row is written unless allowWithoutJournalEntry is true. An API-key caller (the bookkeeping service key) that books Stripe revenue by hand for a charge the Stripe sync already posted, holds, or will post gets 409 STRIPE_CHARGE_ALREADY_BOOKED (#2424). 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 `201`

Event created (journal entry posted, or explicitly skipped via opt-in)

- 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

- `401`: Authentication required
- `403`: Insufficient permissions or not a company member
- `409`: BUSINESS_EVENT_IDEMPOTENCY_CONFLICT (same key, different request) or 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 customer's Stripe balance transactions the sync has posted, holds, or will post. The detail names the Stripe id, the balance transaction, and why. Nothing is written. #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)
