Skip to content

Order to Cash

Order to Cash (O2C) follows a sale from first contact to recognized revenue. Each stage has its own record:

text
deal (pipeline)  →  order (draft → sent → signed → countersigned)  →  contract  →  invoices, drawdowns, change orders  →  revenue schedules
   /o2c/deals           /orders                                          /contracts      /change-orders                      /revenue-schedules, /deferred-revenue
  • A deal is a pipeline card. Converting a won deal creates a customer (if needed), a project and a draft order.
  • An order is the order form: one customer, optionally one project, and lines that each carry a billing rule.
  • Countersigning an order freezes it and creates a live contract. The contract bills, draws down hours and tracks billed vs. recognized revenue.
  • A change order amends a live contract's value and hours after signing.
  • Revenue schedules and deferred revenue show what is billed but not yet earned and when it releases.

This guide covers the main workflows of those route families. See What is not covered for the rest. Money is integer cents unless a field says otherwise, and hours are decimals.

Conventions

Every request needs Authorization: Bearer dz_your_token_here and x-business-id: YOUR_BUSINESS_ID (see Authentication).

Method Permission needed
GET read
POST, PUT, PATCH write
DELETE delete

For a business's own users, viewer has read, member has read and write, and admin and owner also have delete. A read_only token can only call GET. These routes do not require an add-on. Like other write routes, writes are refused with 403 (BILLING_001 or BILLING_002) when the business's trial has ended and it has no active subscription; reads still work.

Successful responses use the standard envelope, {"success": true, "data": ...}. Deletes return {"success": true, "message": "...", "code": "..."}. Errors use the shape in Error Handling. Send an Idempotency-Key header on POST, PUT and PATCH calls you might retry.

Lists use the cursor shape from Pagination (items, next_cursor, prev_cursor, has_next, has_prev, limit, total), except revenue schedules and deferred revenue, which use limit and offset.

Route family Default limit Maximum Notes
/orders, /contracts, /change-orders, /contracts/drawdowns, /contracts/invoices 50 100 Forward only (cursor from next_cursor). total is null. Orders, contracts, change orders and drawdowns list newest first
/o2c/deals, /o2c/deals/pipeline 50 100 total is always filled. sort_by and descending (default true) are accepted. /o2c/deals also takes direction=prev
/revenue-schedules, /deferred-revenue, /deferred-revenue/recognized 100 500 offset paging

Deals

A deal tracks a prospective sale. Early deals use a free-text prospect_name; a deal for an existing customer uses customer_id.

Stage Meaning
prospecting Default for a new deal
qualified, proposal, negotiation Other open stages. An open deal can move to any other stage, forward or backward
closed_won, closed_lost Closed. A closed deal cannot change stage through PATCH

Other enums:

  • source: referral, outbound, inbound, partner, event, expansion, other.
  • revenue_type: recurring, one_time.
  • Lost reason: price, competitor, timing, no_budget, no_decision, poor_fit, other.

Create a deal

bash
curl -X POST "https://api.ondayzero.com/api/v1/o2c/deals" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "prospect_name": "Acme Solar",
    "contact_name": "Jane Rivera",
    "contact_email": "jane@acmesolar.example",
    "source": "referral",
    "owner_name": "Bryce",
    "deal_type": "EPC",
    "revenue_type": "recurring",
    "expected_value_cents": 4800000,
    "expected_hours": 320,
    "expected_term_months": 12,
    "expected_start_date": "2026-12-01",
    "expected_close_date": "2026-11-15"
  }'

Send prospect_name or customer_id; with neither you get 400. stage defaults to prospecting, name defaults to the prospect or customer name, and is_expansion (default false) marks expansion revenue on an existing customer. expected_value_cents defaults to 0 and cannot be negative. owner_name (up to 255 characters) and deal_type (up to 60) are free text. Dates are YYYY-MM-DD.

The response is a deal with id, stage, effective_stage, customer_id, customer_name, project_id, project_name, order_id, the fields above, lost_reason, notes, stage_changed_at, days_in_stage, is_stalled, and the flags is_open, is_closed, is_won, is_lost and is_converted.

effective_stage is closed_won once the deal has an order_id, whatever stage says. is_stalled is true when an open deal has sat in its stage for at least 14 days (prospecting, proposal), 21 days (qualified) or 30 days (negotiation).

List, search and view the pipeline

bash
curl "https://api.ondayzero.com/api/v1/o2c/deals?stage=proposal&is_expansion=false&owner=Bryce&search=acme&limit=50" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"
Query parameter Meaning
stage Exact match on the raw stage (not effective_stage)
is_expansion false for new customers, true for expansion
owner Case-insensitive exact match on owner_name. Use __unassigned__ for deals with no owner
search Case-insensitive substring match on deal name, prospect name or contact name
cursor, limit, direction, sort_by, descending Pagination and sorting
  • GET /api/v1/o2c/deals/owners returns owners (owner_name, deal_count, busiest first) and unassigned_count.
  • GET /api/v1/o2c/deals/pipeline returns metrics plus a deals page. It accepts stage, is_expansion, cursor, limit, sort_by and descending. metrics always covers every open deal, whatever the filters. Open means an open stage and no order_id.
  • GET /api/v1/o2c/deals/{deal_id} returns one deal.
metrics field Meaning
total_pipeline_cents Sum of expected_value_cents over open deals
weighted_pipeline_cents The same sum weighted by stage: prospecting 10%, qualified 25%, proposal 50%, negotiation 75%
open_deal_count Number of open deals
closing_this_quarter_cents Open deals whose expected_close_date falls in the current calendar quarter
new_customer_cents, expansion_cents Open value split by is_expansion

Edit, lose and reopen

PATCH /api/v1/o2c/deals/{deal_id} accepts stage, notes, owner_name, deal_type, revenue_type and the expected_* fields. Omitted fields are untouched. The nullable fields (notes, owner_name, deal_type, revenue_type, expected_hours, expected_term_months, expected_start_date, expected_close_date) are cleared by sending null. expected_value_cents cannot be cleared; a null is ignored. The call is refused with 400 (VALIDATION_450) once the deal has an order_id. Manage the order instead.

bash
curl -X POST "https://api.ondayzero.com/api/v1/o2c/deals/DEAL_UUID/mark-lost" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID" \
  -H "Content-Type: application/json" \
  -d '{"reason": "price", "note": "Went with a cheaper bid"}'

mark-lost requires reason, closes an open deal as closed_lost and appends [Lost: price] <note> to notes. It returns 400 if the deal is already closed. Use it rather than PATCH with stage: closed_lost, because PATCH does not record a lost_reason.

POST /api/v1/o2c/deals/{deal_id}/reopen takes no body. It restores a closed_lost deal to the stage it had before it was marked lost (prospecting if unknown) and clears lost_reason. A deal that is not closed_lost gets 400. A deal that has converted to an order gets 409.

Bulk create

POST /api/v1/o2c/deals/bulk takes {"deals": [...]} with 1 to 250 rows, each shaped like a create body. Rows are independent: a bad row fails alone. An unknown stage, source or revenue_type fails only that row. The response has created, failed and results, where each result has the 1-based row, success, deal_id, name and error.

Convert a deal to an order

bash
curl -X POST "https://api.ondayzero.com/api/v1/o2c/deals/DEAL_UUID/create-order" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID" \
  -H "Content-Type: application/json" \
  -d '{"project_name": "Acme Solar - Phase 1", "order_description": "Phase 1 services"}'

project_name is required (1 to 255 characters). order_description defaults to the deal name. In one atomic step the call:

  1. Uses the deal's customer, or creates one from prospect_name (falling back to the deal name), with contact_email as the customer email.
  2. Creates an active project under that customer. The name must be unique among the customer's projects, ignoring case, or you get 400.
  3. Creates a draft order with the next ORD-NNNN number. order_start_date and term_months come from the deal's expected_start_date and expected_term_months.
  4. Adds one flat line described by order_description. With expected_hours, the line is qty = hours at unit_price_cents = value / hours (rounded to 2 decimals of a cent). Otherwise it is qty = 1 at the full expected_value_cents.
  5. Stamps the deal with customer_id, project_id and order_id, and records a stage event to closed_won.

The deal's own stage column is not changed; read effective_stage or is_converted. The response has customer_id, project_id, order_id and the refreshed deal. A second call returns 400. The new order is a normal draft: edit it, then send, sign and countersign it as below.

Orders

An order is an order form. Statuses:

Status Meaning Next steps
draft Editable send, mark-sent, esign/send, delete
sent Out for signature sign, esign/send, attach-contract-file, remind-signer, revert-to-draft, delete
signed Customer has signed countersign, attach-contract-file, remind-signer, revert-to-draft, delete
countersigned Frozen (frozen: true) and a contract exists Delete only after its contract is deleted

The response carries can_be_sent, can_be_signed, can_be_countersigned, can_be_updated, can_be_reverted and can_be_deleted, so you can check which actions are open without trying them. A call in the wrong status returns 400 with a VALIDATION_* code, for example VALIDATION_359 ("Only draft orders can be sent for signature").

Create an order

bash
curl -X POST "https://api.ondayzero.com/api/v1/orders" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "customer_id": "019ab37c-c057-7000-8000-000000000001",
    "project_id": "019ab37c-c057-7000-8000-000000000002",
    "order_description": "Implementation and platform subscription",
    "order_start_date": "2026-11-01",
    "first_invoice_date": "2026-11-01",
    "term_months": 12,
    "discount_cents": 50000,
    "currency": "USD",
    "lines": [
      {
        "description": "Prepaid implementation hours",
        "qty": 100,
        "unit_price_cents": 15000,
        "billing_rule": "prepaid_block"
      },
      {
        "description": "Platform subscription",
        "qty": 1,
        "unit_price_cents": 200000,
        "billing_rule": "recurring",
        "billing_timing": "each_period"
      }
    ]
  }'

Only customer_id is required. The order is created as draft with the next ORD-NNNN number. The customer must exist in the business and not be deleted, and project_id must belong to the business (404 otherwise).

Field Notes
order_start_date, first_invoice_date YYYY-MM-DD. first_invoice_date anchors the term clock; end_date is that date plus term_months (falling back to order_start_date)
term_months Integer, at least 1
discount_cents Default 0. A discount above the subtotal is 400
tax_treatment Free-text label
currency 3-letter code, upper-cased. Default USD
lines Order lines (below). Can be empty on a draft, but an order needs at least one line before it can be sent

Each line has qty (decimal, up to 2 decimal places, at least 0), unit_price_cents (at least 0, up to 2 decimal places so a rate like 5.25 means $0.0525), description, catalog_item_id, billing_rule (default flat), billing_timing and ramp_schedule (a JSON object for pre-agreed step quantities or prices).

billing_rule How the contract bills it (from the contract billing code)
flat One amount on the contract's first (execution) invoice
prepaid_block On the execution invoice, booked to Deferred Revenue and recognized as approved hours burn down. Its qty counts toward the contract's contracted hours
recurring billing_timing is each_period (billed on each period invoice), first_upfront or all_upfront (the latter two bill on the execution invoice)
per_unit Billed on period invoices from approved drawdown hours times unit_price_cents, capped at qty. If an order has no upfront lines, the execution invoice instead bills the bought quantity and sending it books it to Deferred Revenue
usage_metered Accepted and stored. The contract draft endpoints do not draft it automatically; bill it with a manual contract invoice

Each line in the response also has amount_cents (qty times unit_price_cents, rounded half-up to whole cents) and catalog_item_name. The order's subtotal_cents is the sum of line amounts and is not multiplied by term_months; order_value_cents is the subtotal minus discount_cents, never below 0.

List, get and update

bash
curl "https://api.ondayzero.com/api/v1/orders?status=sent&customer_id=CUSTOMER_UUID&limit=50" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"

status and customer_id are the only filters. status is matched exactly against draft, sent, signed or countersigned; an unknown value returns an empty page. GET /api/v1/orders/{order_id} returns the order with its lines, contract_id (once countersigned), customer_name, project_name and project_code.

PUT /api/v1/orders/{order_id} edits a draft only (400, VALIDATION_358 otherwise; revert a sent or signed order first). It takes the same fields as create, all optional. Omitted fields are untouched. project_id, the dates, term_months, order_description and tax_treatment are cleared with null. If you send lines, they replace all existing lines, and the subtotal is recomputed. The discount is re-checked against the subtotal.

Send, sign and countersign

Endpoint Body Effect
POST /orders/{id}/send none draft to sent; sets sent_at
POST /orders/{id}/mark-sent none The same transition
POST /orders/{id}/sign optional {"signed_contract_file_url": "..."} sent to signed; sets signed_at
POST /orders/{id}/attach-contract-file {"signed_contract_file_url": "..."} Stores the executed contract reference on a sent, signed or countersigned order. On a sent order it also moves the order to signed
POST /orders/{id}/countersign optional {"signed_contract_file_url": "..."} signed to countersigned; freezes the order and creates the contract
POST /orders/{id}/revert-to-draft none sent or signed back to draft; clears sent_at, signed_at and signed_contract_file_url
POST /orders/{id}/remind-signer none Emails the customer a signature reminder
DELETE /orders/{id} none Removes a draft, sent or signed order, or a countersigned one whose contract was deleted

What "send" actually does. send and mark-sent are status changes only. Both require the order to be draft and to have at least one line, then set status to sent and sent_at. Neither emails the customer, creates a document or contacts a signature provider. The code notes that send is where a customer notification would go in future, and that mark-sent must never notify. Today the two behave identically. To email the customer a signing request, use the e-signature endpoint below.

Signing is recorded, not collected. sign does not capture a signature. You call it, with an optional reference to the executed PDF, once the customer has signed outside DayZero. The e-signature webhook calls it for you when an envelope completes. To register an executed PDF, upload it (see Uploads), then pass the returned s3_key or a URL as signed_contract_file_url to sign, attach-contract-file or countersign.

Remind the signer. remind-signer works only while the order is sent or signed, at most once per 24 hours, and needs an email address on the customer. It sets signer_reminder_sent_at. Other cases return 400.

Countersign. countersign locks the order row, so two concurrent calls cannot create two contracts. It sets frozen: true and countersigned_at, then creates the contract (below) and emits the order.countersigned webhook (see Webhooks). The response is the order with contract_id set. If first_invoice_date was never set, the order's order_start_date is used, or today's date if that is also missing. The contract starts as awaiting_first_invoice. The contract's original_value_cents and current_value_cents start at the order's order_value_cents.

E-signature for orders

An envelope is a signature request created at the e-signature provider. The provider is DocuSign.

bash
curl -X POST "https://api.ondayzero.com/api/v1/orders/ORDER_UUID/esign/send" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID" \
  -H "Content-Type: application/json" \
  -d '{"document_url": "https://files.example.com/order-form.pdf", "subject": "Please sign: ORD-0007"}'

document_url is required: either a public https URL, or an s3_key from an upload for the same business. DayZero does not render order PDFs, so you supply the document. Private, internal and cloud-metadata addresses are rejected. subject is optional (up to 200 characters; the default names the order).

The call creates an envelope addressed to the customer's email, stores esign_envelope_id, esign_status and esign_sent_at on the order, and, if the order was draft, moves it to sent. It returns 400 when:

  • e-signature is not configured for the environment (VALIDATION_426);
  • the order is not draft or sent (VALIDATION_427);
  • the customer has no email (VALIDATION_428);
  • an envelope is already sent or delivered for the order (VALIDATION_430);
  • the order has no lines.

If the provider rejects the envelope, the call fails with 500.

GET /api/v1/orders/{order_id}/esign returns the envelope state: order_id, order_status, provider, envelope_id, esign_status (sent, delivered, completed, declined or voided), esign_sent_at and refreshed. It reads live from the provider when credentials are configured (refreshed: true) and otherwise returns the stored snapshot. It returns 400 if the order has no envelope (VALIDATION_431).

The provider reports back through POST /api/v1/webhooks/esign. That endpoint is for the provider, not for your integration: it is unauthenticated, checks an HMAC signature, and returns 403 without a valid one. A completed envelope moves a sent order to signed and stores the executed document URL when the provider supplies one. declined and voided only update esign_status. If the order was reverted to draft after sending, a completed event updates esign_status but leaves the order's status alone. Countersigning is never automatic: call countersign yourself after the order reaches signed.

Contracts

A contract is the live form of a countersigned order. Statuses:

Status Meaning
awaiting_first_invoice Created at countersign; nothing billed yet
active_drawing_down First invoice sent and it included a prepaid block
active_billing First invoice sent with no prepaid block
paused Billing and drafting stopped
completed Term elapsed, or the prepaid block is fully consumed and billed

Contract responses include order_id, project_id, customer_id, customer_name, number (CON-NNNN), status, currency, original_value_cents, current_value_cents, billed_cents, recognized_cents, deferred_remaining_cents (billed minus recognized, never below 0), billed_pct, earned_pct (fractions, for example 0.25), total_contracted_hours, hours_drawn, hours_remaining, first_invoice_date, end_date, and the flags can_be_paused, can_be_resumed and can_be_amended.

Create or promote a contract

A contract always comes from a countersigned order. POST /api/v1/contracts does the whole order lifecycle for you. Send exactly one of order_id or order; otherwise you get a 400.

bash
# Promote an existing order
curl -X POST "https://api.ondayzero.com/api/v1/contracts" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID" \
  -H "Content-Type: application/json" \
  -d '{"order_id": "019ab37c-c057-7000-8000-000000000010"}'
  • order_id promotes an existing draft, sent or signed order. The call skips any step that is already done: it sends the order if it is a draft, marks it signed if it is sent, then countersigns it. It returns 400 if the order already has a contract (VALIDATION_364) or has no lines.
  • order takes a full order body, exactly as in Create an order. It creates the order, then runs the same send, sign and countersign steps. This is for agreements that were signed offline or are being migrated.

Promotion records the signing steps without any customer-facing step: nothing is emailed and no signature is collected. The response is the new contract. Both paths emit order.countersigned.

List, get, edit, pause and delete

  • GET /api/v1/contracts accepts status (exact match on a status above), customer_id, cursor and limit.
  • GET /api/v1/contracts/{contract_id} returns one contract.
  • PATCH /api/v1/contracts/{contract_id} edits number (up to 50 characters), first_invoice_date, end_date (both clearable with null; end_date before first_invoice_date is 400) and line_items. A completed contract cannot be edited.
    • line_items edits the originating order's lines in place. Each item with an id edits that line's description, qty or unit_price_cents. An item without an id appends a new line and then needs qty and unit_price_cents, plus optional catalog_item_id, billing_rule, billing_timing and ramp_schedule. Lines you omit are untouched.
    • qty and unit_price_cents changes, and appended lines, are accepted only while no money has moved (nothing billed or recognized, no approved drawdowns); afterwards use a change order (400). On success, the original and current values and the contracted hours are recomputed.
    • status is a shortcut for the lifecycle actions: completed, on_hold or paused, or active or resume. If status is present, only the status change is applied and the other fields in that call are ignored.
  • POST /api/v1/contracts/{contract_id}/pause works on an active contract (awaiting_first_invoice, active_drawing_down, active_billing). POST .../resume works on a paused or completed contract, and restores awaiting_first_invoice if nothing has been billed, active_drawing_down if the contract has contracted hours, and active_billing otherwise.
  • PATCH /api/v1/contracts/{contract_id}/hours takes {"total_hours": 120} and sets the contracted hours (the burndown denominator). It cannot go below the hours already drawn (400) and is refused on a completed contract.
  • POST /api/v1/contracts/{contract_id}/check-completion completes the contract if the term has elapsed or the prepaid block is consumed and billed, and returns the contract.
  • DELETE /api/v1/contracts/{contract_id} works only while nothing is billed or recognized, no non-draft invoice is linked (voided and uncollectible ones do not count), and no drawdown is approved. Linked draft invoices and pending or returned drawdowns are deleted with it, and the order is kept as a countersigned order without a contract.

Invoices

Contract invoices pass two human gates before money moves: reviewing the invoice, and approving hours.

  1. Draft the first invoice. POST /api/v1/contracts/{contract_id}/draft-invoices (no body) drafts one execution invoice containing the flat, prepaid_block and upfront recurring lines, skipping zero-amount lines. It returns 400 if one was already drafted (VALIDATION_388), if there is nothing billable, or if the contract is completed. Drafting posts nothing to the ledger.
  2. Draft later periods. POST .../generate-period-invoices takes {"period_date": "2026-12-15"}, any date inside the period, and drafts that period's invoice from each_period recurring lines and per_unit lines (approved, not yet billed hours times unit price, capped at the bought quantity). The period follows the business's invoice_cadence setting, either calendar_month or four_week. It is idempotent per period, only works on an active contract, and only inside the contract term. POST .../invoices drafts an extra manual invoice with optional due_date (default 30 days out) and line_items of description, amount_cents and quantity; without lines it uses the each_period recurring lines.
  3. Review. GET /api/v1/contracts/invoices lists contract invoices, with contract_id, status (default draft; all for every status), cursor and limit. Items include number, status, total, amount_paid, balance_due, due_date, can_be_sent and line_items (description, quantity, unit_price, amount, billing_rule, order_line_id, prepaid). PATCH /api/v1/contracts/invoices/{invoice_id} edits a draft's description, due_date and line_items (description, quantity, amount_cents, billing_rule, order_line_id). The lines replace the old ones, and the total is recalculated.
  4. Approve and send. POST /api/v1/contracts/invoices/{invoice_id}/send moves draft to open. The amount becomes billed on the contract. The prepaid portion is booked to Deferred Revenue, and the rest is booked to contract revenue and counted as recognized. The first sent invoice activates the contract.

If the business's O2C settings name an invoice_approver_role, only that role or an owner can call send; others get 403 (VALIDATION_406). Settings are read and written at GET and PUT /api/v1/contracts/settings.

Drawdowns (hours)

A drawdown records hours consumed against a contract.

bash
curl -X POST "https://api.ondayzero.com/api/v1/contracts/CONTRACT_UUID/drawdowns" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID" \
  -H "Content-Type: application/json" \
  -d '{"hours": 6.5, "entry_date": "2026-11-18", "description": "Integration work", "submitted_by": "dana@example.com", "submitted_via": "manual"}'

hours (above 0) and entry_date (YYYY-MM-DD) are required. submitted_via is manual (default), slack or email. order_line_id attributes the hours to a line of this contract's order. It is validated, and unattributed hours draw the prepaid block. The entry is created pending. Recording is allowed on any contract that is not completed.

  • GET /api/v1/contracts/{contract_id}/drawdowns lists a contract's entries, and GET /api/v1/contracts/drawdowns is the portfolio-wide queue. Both take status (pending, approved or returned), cursor and limit.
  • POST /api/v1/contracts/drawdowns/{drawdown_id}/approve takes an optional {"hours": 6, "approved_by": "..."}. A corrected hours keeps the submitted value in original_hours and sets was_edited. Approval draws down the block and recognizes revenue by the hours-percent method: approved hours / total contracted hours x prepaid amount booked. Recognition only moves up, never down. It emits drawdown.approved.
  • POST .../drawdowns/{drawdown_id}/return sets a pending entry to returned.

Approval returns 400 when the contract is paused or completed, when nothing has been billed yet (VALIDATION_380), or when the hours would exceed the block (VALIDATION_371) or the attributed line's bought hours. If hours_approver_role is set in the O2C settings, only that role or an owner can approve or return (403, VALIDATION_405). Approving the last hours of a block completes the contract.

Reporting endpoints

Endpoint Returns
GET /contracts/{id}/burndown block (total_hours, used_hours, remaining_hours, unattributed_hours, recognized_cents, deferred_remaining_cents) and per-line lines
GET /contracts/summary Portfolio totals: contract value, billed, recognized, deferred, counts and counts_by_status
GET /contracts/summary/by-type Contracted line value grouped by billing rule
GET /contracts/cash-flow-forecast?weeks=13 Expected weekly inflow per contract (weeks from 1 to 52)
GET /contracts/{id}/suggested-invoices Unlinked invoices ranked as likely matches, with score and reasons

Change orders

A change order is a versioned amendment to a live contract. Statuses are draft, sent, signed and countersigned. Versions start at 1 per contract.

bash
curl -X POST "https://api.ondayzero.com/api/v1/change-orders" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "contract_id": "019ab37c-c057-7000-8000-000000000020",
    "description": "Add 40 prepaid hours",
    "value_delta_cents": 600000,
    "hours_delta": 40
  }'

contract_id and value_delta_cents (an integer that may be negative) are required. hours_delta is a signed decimal, or null/omitted to leave hours alone. delta_lines is a free-form JSON object for describing adds, removes and quantity changes. It is stored but never applied: only value_delta_cents and hours_delta change the contract. Creating is refused with 400 (VALIDATION_367) if the contract is completed; paused contracts can be amended.

Endpoint Effect
GET /change-orders List; filters contract_id, status, plus cursor and limit
GET /change-orders/{id} One change order (version, signed_file_url, sent_at, signed_at, countersigned_at, can_be_* flags)
PUT /change-orders/{id} Edit description, value_delta_cents, hours_delta or delta_lines on a draft. Send hours_delta: null to clear it
POST /change-orders/{id}/send Emails the change order to the contract customer (customer_email on the response), then moves draft to sent. On a sent change order it emails again (status unchanged, sent_at updated); replies go to the user who sent it
POST /change-orders/{id}/sign sent to signed
POST /change-orders/{id}/countersign signed to countersigned, and applies the deltas
POST /change-orders/{id}/revert-to-draft sent or signed back to draft; clears the timestamps and signed file
DELETE /change-orders/{id} Drafts only

None of these calls take a body. send is the only one that notifies anyone: it emails the customer, and the status and sent_at change only after the email goes out. sign and countersign collect no signature; they only record the step. Signed and countersigned change orders cannot be sent.

HTTP Code Cause
400 VALIDATION_873 The contract's customer has no email address
400 INTEGRATION_092 The email could not be delivered; nothing changed
403 Demo business
409 CONFLICT_101 Emailed less than 5 minutes ago

Countersign applies the delta, exactly once. The change order and the contract are both locked. current_value_cents becomes current_value_cents + value_delta_cents, and total_contracted_hours becomes total_contracted_hours + hours_delta when hours_delta is set. original_value_cents never changes. The call fails with 400, and nothing is applied, if:

  • the new current value would be below 0 (VALIDATION_404);
  • the new hours total would be below the hours already drawn (VALIDATION_408);
  • the contract is completed.

Hours-percent recognition uses the new hours total from the next drawdown approval onward. Revenue already recognized is not written back.

Revenue schedules and deferred revenue

DayZero has two read/write families for deferred revenue. Both book to a Deferred Revenue liability and release it to revenue with a recognition journal entry, Dr Deferred Revenue / Cr Revenue.

Revenue schedules (/revenue-schedules)

A revenue schedule is one tranche with source, source_key, recognition_method, period_start, period_end, amount_cents, currency, revenue_ledger_name, recognized, recognized_at, journal_entry_id and booking_journal_entry_id. A tranche becomes due when period_end is on or before the run date.

source Created by
preorder POST /revenue-schedules/preorders
manual Contracts. Each prepaid block sent on a contract invoice creates a parked hours_percent tranche (reference_type: "contract"); each hours recognition and each recognized-at-billing portion is recorded as an already-recognized tranche
invoice Invoice deferrals (below)
asc606, shopify_subscription, stripe_subscription, recharge Other sources the engine can hold. They are filterable in lists and the waterfall

recognition_method is straight_line, point_in_time or hours_percent. GET /api/v1/revenue-schedules takes source, limit (default 100, max 500) and offset, and returns schedules and count (the rows on this page).

Pre-orders. Book cash received ahead of fulfillment:

bash
curl -X POST "https://api.ondayzero.com/api/v1/revenue-schedules/preorders" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID" \
  -H "Content-Type: application/json" \
  -d '{"order_reference": "PRE-1001", "amount_cents": 250000, "order_date": "2026-11-01", "receive_via": "cash"}'

order_reference (your stable identifier) and amount_cents (above 0) are required. receive_via is the account debited: clearing (default), cash or ar. revenue_ledger_name defaults to Pre-order Revenue, currency to USD, and order_date to today. It posts Dr <cash, AR or clearing> / Cr Deferred Revenue and parks one tranche. The call is idempotent on order_reference: repeating it returns the existing tranche.

Release it on shipment with POST /api/v1/revenue-schedules/preorders/fulfill and {"order_reference": "PRE-1001", "fulfillment_date": "2026-11-20"} (the date defaults to today). It sets the tranche due on that date and posts the recognition entry. An unknown or already recognized reference is a no-op, not an error: look at rows_posted in the response (0 means nothing was posted; recognized_cents is always 0 on this endpoint, so read the amount from the tranche).

Run recognition. POST /api/v1/revenue-schedules/recognize (no body) posts every due tranche in the business whose source the engine posts for: preorder, manual, invoice, stripe_subscription and recharge. Other sources are posted by their own jobs. The response is success, as_of_date, rows_due, rows_posted, recognized_cents and errors. A tranche that fails is listed in errors and the rest still post. Re-running is safe because a posted tranche is skipped.

Waterfall. GET /api/v1/revenue-schedules/waterfall (optional source) returns buckets of period (YYYY-MM) and amount_cents for tranches not yet recognized, oldest first, plus total_deferred_cents. Parked tranches that wait on fulfillment or on hours (pre-orders and prepaid blocks) sit at period_end 9999-12-31 and show under 9999-12.

Invoice deferrals (/deferred-revenue)

A deferral moves an invoice's booked revenue into Deferred Revenue and schedules its release. Posting and scheduling happen on POST. Recognition posts later.

bash
curl -X POST "https://api.ondayzero.com/api/v1/deferred-revenue/invoices/INVOICE_UUID" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID" \
  -H "Content-Type: application/json" \
  -d '{"unit": "months", "method": "straight_line", "period_count": 12, "start_date": "2026-12-01", "notes": "Annual support"}'
Field Notes
period_count Required, 1 to 600 monthly periods
deferred_amount_cents Defaults to the invoice's revenue (total less sales tax). It cannot exceed that amount
unit months (default), hours or custom
total_units The denominator. Defaults to period_count for months; required for the other units
method straight_line (default) fills every period evenly. custom leaves each period's units empty, and nothing posts until they are filled in
start_date First period. Defaults to the current month
revenue_ledger_name Defaults to the account the invoice booked revenue to
periods Optional per-period period_start and units for a custom schedule; its length must equal period_count

The invoice must already have booked revenue (not draft, void or uncollectible), or you get 400 (VALIDATION_457). An invoice that already has a deferral returns 409 (CONFLICT_034). The response is the deferral: deferred_amount_cents, recognized_cents, remaining_cents, status (active, completed or cancelled), the terms, journal entry IDs, and periods (sequence, period_start, period_end, units, amount_cents, recognized, recognized_at, journal_entry_id).

Endpoint Purpose
GET /deferred-revenue Deferrals with their periods. Filters status, customer_id; limit (default 100, max 500) and offset. Also returns count and total deferred, recognized and remaining cents
GET /deferred-revenue/{deferral_id} One deferral
GET /deferred-revenue/invoices/{invoice_id} is_deferred, deferrable_revenue_cents (the cap), the live deferral, and suggested_deferred_amount_cents (the sum of the invoice's lines tagged deferred; null once a deferral exists)
PUT /deferred-revenue/{deferral_id} Re-cut the not-yet-posted periods: unit, method, period_count, total_units, start_date, revenue_ledger_name, notes. Posted periods are untouched
POST /deferred-revenue/{deferral_id}/periods/{period_id} Custom schedules: set units, optionally recognition_date and post_now (default true), so the period can post
POST /deferred-revenue/{deferral_id}/cancel Body {} or {"as_of": "2026-12-31"}. Releases the remaining balance to revenue and deletes the unposted periods
POST /deferred-revenue/recognize Body {} or {"as_of": "2026-12-31"}. Posts every period whose end date has passed. Returns as_of, periods_due, periods_posted, recognized_cents and errors. Safe to re-run
GET /deferred-revenue/matrix Invoices against recognition months. status defaults to active. Returns periods, rows, period_totals and customer_totals
GET /deferred-revenue/recognized Recognized and upcoming revenue grouped by month, method and source (invoice or contract). Filters start_date and end_date (on period end), limit and offset; returns rows, method_totals and totals

Contract prepaid blocks do not appear under /deferred-revenue deferrals. They are revenue-schedule tranches; GET /deferred-revenue/recognized includes them with source: "contract".

O2C reports (/contracts/reports)

GET /api/v1/contracts/reports/{report_type}?format=csv|xlsx streams a file download (csv is the default). report_type is contract_register, ar_aging, deferred_revenue_waterfall, hours_ledger or billing_schedule; an unknown type or format is a 400. Each download is written to the audit log as an export. These are the same files as the Reports page of the Order to Cash workspace.

From an AI agent (MCP)

Deals, orders and contracts have MCP tools (see MCP Server); every write takes dry_run and goes through approval. Change orders, drawdowns, revenue schedules and deferred revenue are REST only.

Task Tool
Read list_deals / get_deal, list_orders / get_order, list_contracts / get_contract
Deals create_deal, bulk_create_deals, update_deal, bulk_update_deals, convert_deal_to_order (deal_id, project_name)
Orders create_order, bulk_create_orders, update_order (lines replaces all lines), advance_order_status (target_status: draft, sent, signed or countersigned), attach_order_document (order_id, s3_key)
Contracts create_contract (order_id or order), update_contract, bulk_create_contracts, bulk_update_contracts, link_invoice_to_contract

Errors

Business-rule failures are 400 with a stable code; branch on code, not on message. A missing record is 404 (NOT_FOUND_*), including a record that belongs to another business. Duplicate deferrals are 409. The O2C codes you are most likely to see:

Code Meaning
VALIDATION_358 / VALIDATION_359 / VALIDATION_360 / VALIDATION_361 Order cannot be updated / sent / signed / countersigned in its current status
VALIDATION_362 Countersigned order with a live contract cannot be deleted
VALIDATION_363 Order has no lines
VALIDATION_364 Order already has a contract
VALIDATION_399 Discount exceeds the order subtotal
VALIDATION_367 / VALIDATION_368 Contract is completed and cannot be amended / the action needs an active contract
VALIDATION_390 Contract has billed or recognized revenue and cannot be deleted
VALIDATION_405 / VALIDATION_406 Your role may not approve hours / send contract invoices (403)
VALIDATION_450 Deal was converted to an order and is read-only

Use Accept: application/json; see Error Handling for the full envelope and retry rules.

What is not covered

Routes under /contracts for reordering a prepaid block, issuing credit memos, recording payments on contract invoices, payment reminders, and inbound hours or order PDF intake are not described here. Neither are the outbound O2C webhook endpoints under /webhooks/o2c (see Webhooks) nor the Slack hours receiver. The OpenAPI reference lists every operation and schema.