Skip to content
campaiDeveloper
campai.com

Create a batch as individually tracked draft invoices. Validate the input first, create one draft at a time, save each returned ID, and read it back. Issuing, sending, and collecting payment are separate business steps and are outside the draft-creation example below.

The invoice create path is POST /{organizationId}/{mandateId}/receipts/invoice. Notice that this write path does not contain /finance, while Get receipt and List receipts do. Use the exact operation paths rather than deriving one from another.

Read Understand money, dates, and data types before mapping amounts, dates, IDs, or missing fields into your own system.

What you’ll have at the end

A verified draft invoice and a row-by-row batch procedure with explicit recovery for uncertain writes.

  1. Reviewed row
  2. Pending ledger entry
  3. Create draft
  4. Read back and verify
Before you start
  • Mandates Read and Invoices and receipts Insert/Read
  • A synthetic debtor and valid accounting allocations
  • A durable local ledger before automating a batch
On this page

Create a key using Make your first API request. The key needs Mandates → Read and Invoices and receipts → Insert and Read. Use a test mandate with a designated synthetic debtor and a valid revenue account from its chart of accounts.

There are two different account numbers: top-level account identifies the debtor/customer account; each position's account identifies its accounting allocation. A contact _id is neither of these. Verify the accounts and accounting configuration in campai before calling the endpoint.

For each row in your input, provide a stable external operation ID and a reviewed invoice object containing account, address, deliveryDateType, title, intro, receiptDate, dueDate, and positions. A position needs unitAmount, discount, description, and account; set quantity explicitly for clarity. The API reference specifies the complete field types and limits.

Amounts are integer minor units, so an amount of 1000 represents 10.00 in the mandate's currency. Do not send formatted currency strings or silently relabel a value from another currency. Use the organization's actual tax, account, and date configuration; this example does not choose those rules for you.

Some accounting plans require costCenter1 on each position because they use regions. This is enforced even for drafts. A structurally valid payload without the required cost center returns 400. Read the configured chart of accounts and mandate cost centers, then supply the intended allocation; do not copy an arbitrary region number from another organization.

Keep a durable job record for each row in your own database. Use a unique (organizationId, mandateId, externalId) constraint so two workers cannot create the same invoice. Store the reviewed payload and mark the row pending before sending it. If the row already exists, inspect its outcome instead of sending another create.

Start with a two-row test batch and use this state model in your own database before adding a loop:

Row state Stored evidence Next action
pending Unique external ID, scope, reviewed payload Send one create only after claiming the row exclusively.
created Returned receipt _id Read that receipt back; do not create again.
verified Matching draft, account, positions, amounts Skip the row on a rerun.
uncertain Timeout or lost response, with the original payload Inspect campai and resolve the outcome before retrying.
rejected A definite validation error Correct and review the payload before an explicit new attempt.

First complete run: create and verify row A using the requests below, save its ID, then do the same for row B. Restart your worker and confirm both rows are skipped. Stop after those two drafts and compare them in campai before scaling the batch. The examples implement the draft requests; the persistent ledger and worker are part of your own service.

Replace the key, organization, and mandate placeholders. Replace account 100001 with your designated debtor account and position account 40000 with the intended revenue account. Review the address, dates, amounts, and any required cost centers before running this write. Each tab sends one draft request; it does not implement your batch ledger.

Example

curl --fail-with-body --silent --show-error --max-time 30 --globoff \
  --request POST 'https://cloud.campai.com/api/YOUR_ORGANIZATION_ID/YOUR_MANDATE_ID/receipts/invoice' \
  --header 'X-API-Key: YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "account": 100001,
  "accountName": "Example customer",
  "address": {
    "country": "DE",
    "zip": "10115",
    "city": "Berlin",
    "addressLine": "Example street 1"
  },
  "deliveryDateType": null,
  "title": "Integration test",
  "intro": "",
  "receiptDate": "2026-11-04T00:00:00.000Z",
  "dueDate": "2026-11-18T00:00:00.000Z",
  "positions": [
    {
      "unitAmount": 1000,
      "quantity": 1,
      "discount": 0,
      "description": "Integration test item",
      "account": 40000
    }
  ],
  "isNet": false,
  "draft": true,
  "sendMethod": "none",
  "email": "",
  "doNotSendReceipt": true,
  "useDepositReceipts": false,
  "paymentMethod": null,
  "queueReceiptDocument": false
}'

Save the returned _id against the pending row before continuing. The payload disables delivery, deposit application, and finalization. Do not spread arbitrary input into it or change draft to false to bypass a validation error.

Replace YOUR_RECEIPT_ID with the ID just returned and use the same key, organization, and mandate:

Example

curl --fail-with-body --silent --show-error --max-time 30 --globoff \
  --request GET 'https://cloud.campai.com/api/YOUR_ORGANIZATION_ID/YOUR_MANDATE_ID/finance/receipts/YOUR_RECEIPT_ID' \
  --header 'X-API-Key: YOUR_API_KEY'

Check type: invoice, draft: true, the debtor account, positions, and amounts. Mark the row verified only after that check succeeds, then process the next row. Stop the batch on an error and keep the IDs of already verified drafts.

For a row with a receipt ID, retrieve that receipt and finish verification. For a pending row without an ID, the create may already have committed. Reconcile the intended invoice in campai before retrying. Do not erase the pending row or assign a new external ID to bypass the duplicate check.

Resume only rows whose outcomes you have resolved. The local job record prevents duplicate work in your integration; it does not imply a server-side idempotency header.

Inspect draft amounts, customer, addresses, positions, dates, accounting allocation, and tax treatment in campai. Compare the number of verified drafts with the intended batch and review any unmatched or uncertain rows.

Only then design an authorized finalization and delivery process using the current receipt operations. Draft creation is not proof that an invoice was issued, emailed, paid, or posted. Do not describe a successful create response as “payment completed.”

Test a valid draft, an invalid account, malformed position, duplicate external ID, a lost create response, and a process restart after success. Confirm no duplicate draft is created and no email or payment action occurs. Remove only your designated test drafts with Delete receipt and the required remove permission after testing.

Check your result

Successful row
One draft exists; no sending or payment occurs.
Lost create response
The row stays uncertain until reconciled.
Repeated external ID
Inspect the existing ledger row instead of creating another invoice.
Continue with Find and filter receipts