Skip to content

Mark an occurrence as already paid out of band (and link its payment if already posted)

POST
/bills/{id}/mark-scheduled
curl --request POST \
--url https://api.tovarifinancial.com/bills/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0/mark-scheduled \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "occurrenceDate": "example", "scheduledPaymentDate": "example", "amountCents": 1, "note": "example", "skip": true, "forecastDate": "example", "forecastAmountCents": 1 }'

The THIRD disposition of an outstanding occurrence, beside Enter and Skip. It records the user’s own assertion that they already paid this occurrence — typically weeks early — with the date and amount they actually paid, and every forecast surface then PRESENTS the occurrence at those values. The declaration itself creates, edits or reserves no transaction. The occurrence keeps its ORIGINAL occurrenceDate as its identity in every list, total, horizon and month it belongs to. Upsert on (bill, occurrence): re-marking with corrected values updates in place. ONE UNATTENDED SETTLE MAY FOLLOW (KAN-2748): when the marked occurrence is the bill’s first outstanding one and a CLEARED bank row already in the register matches it — against the schedule’s date and amount OR the declared ones, with name evidence, at high confidence and with no tie — the server links that row exactly as bank sync would, in the same transaction: the occurrence is settled at the row’s posted date, bills.nextPaymentDate advances past it, and the mark is cleared. settledAgainst then names that row; it is null when nothing settled, and the mark stands. THAT SETTLE RUNS ONLY WHEN THIS REQUEST SUPPLIED A DECLARATION PAIR — a note-only or skip-only call never claims a bank row.

THE BODY IS A PRESENCE-KEYED PATCH, NOT A PUT (KAN-2913). An absent key leaves that facet exactly as stored; a key present with null / empty / false clears it; scheduledPaymentDate and amountCents are a PAIR and sending one without the other is a 400. FOUR facets ride alongside the declaration — a free-text note, a skip flag, and the FORECAST-ONLY forecastDate / forecastAmountCents override (KAN-2926), which are INDEPENDENT of each other and NOT subject to the pair rule. When the MERGED post-state carries no declaration, no note, skip false and neither forecast facet, the stored row is DELETED and the response reads every facet at its cleared constant. Clear-on-empty is decided by ABSENCE alone for the DECLARATION — it is never “the amount equals the schedule’s amount”. THE FORECAST FACETS ARE THE OPPOSITE: an estimate equal to the series IS the series, so a forecastDate equal to occurrenceDate, or a forecastAmountCents equal to the schedule’s occurrence amount, is stored as no override and reads back null. A forecast write never sets scheduledPaymentDate, never badges “already paid”, and never triggers the settle.

X-Idempotency-Key is HONORED, and its fingerprint is the NORMALIZED PATCH WITH ABSENT KEYS OMITTED — the bill and occurrence plus whichever of the declaration pair, note, skip and the two forecast facets were actually sent, taken over the SUBMITTED patch before the equality-to-schedule collapse — rather than skip’s two values. A mark is a form submission whose values ARE the request, so a resubmission with a corrected amount under the same key is refused 409 IDEMPOTENCY_CONFLICT rather than silently replaying the first envelope; omitting absent keys is what keeps a bare date+amount retry a replay rather than a 409. A replay returns the recorded body, settledAgainst included. The response carries no bill object and no effective values: clients refetch rather than patch.

id
required
string format: uuid

Bill id

X-Idempotency-Key
string

Optional idempotency key for safe write retries. Every operation that declares this header reads it. Three cases, told apart per operation: (1) where the operation also documents a 409 whose codes include IDEMPOTENCY_CONFLICT, replaying this key with a different payload returns 409 IDEMPOTENCY_CONFLICT and replaying it with the same payload returns the original response; (2) where it documents no 409, a replay is reconciled optimistically against a unique key column and answers the original 200; (3) where it documents a 409 whose codes do NOT include IDEMPOTENCY_CONFLICT, a conflict on this key is impossible — a replay answers a 200, and that 409 is a resource-state conflict described by the operation’s own 409 schema.

Media typeapplication/json
object
occurrenceDate
required

The ORIGINAL scheduled occurrence date — the identity of the occurrence being marked, and the same value /enter and /skip take. NEVER the declared payment date.

string
scheduledPaymentDate

When the user says they actually paid it. Unbounded in both directions by design (paying 30 days early is the point, and recording last week is equally valid); only a malformed date is refused, and no relation to occurrenceDate, to today, or to the next occurrence is enforced. PAIRED with amountCents: send both or neither, both values or both null. Omit the pair to leave an existing declaration untouched; send both as null to withdraw it.

string
nullable
amountCents

POSITIVE MAGNITUDE, integer minor units. Must be an integer > 0 when present and non-null. PAIRED with scheduledPaymentDate — sending one without the other is a 400.

integer format: int64
nullable
note

The household’s own free text about THIS occurrence. Trimmed; empty or whitespace-only stores no note; at most 500 characters counted as CODE POINTS; a string containing U+0000 is refused. Omit the key to leave an existing note untouched; send null or an empty string to clear it. Never echoed in an error message and never logged.

string
nullable
skip

“Do not count this occurrence in the forecast.” PRESENTATION ONLY: it zeroes the row’s contribution to the running projected balance on every balance-bearing surface and excludes it from GET /bills/scheduled’s totalDueCents and GET /bills/projection’s remainingDueThisMonthCents. It is NOT POST /bills/{id}/skip — it never advances bills.nextPaymentDate, never writes payment history, and never removes the row from any list. Omit the key to leave the stored flag untouched.

boolean
forecastDate

FORECAST-ONLY override of WHEN this occurrence lands (KAN-2926). It asserts NO payment: it never sets scheduledPaymentDate, never badges the occurrence “already paid, waiting to clear”, and never triggers the unattended settle. INDEPENDENT of forecastAmountCents — a date-only override is normal and is NOT subject to the declaration pair rule. Unbounded in both directions; only a malformed date is refused. A value equal to occurrenceDate is stored as no override at all and reads back null. Omit the key to leave an existing override untouched; send null to clear it.

string
nullable
forecastAmountCents

FORECAST-ONLY override of HOW MUCH this occurrence is (KAN-2926), as a POSITIVE MAGNITUDE in integer minor units — the sign is re-derived from the bill’s direction by the server and is never sent or inferred by a client. INDEPENDENT of forecastDate. A value equal to the schedule’s own occurrence amount is stored as no override and reads back null. Omit the key to leave an existing override untouched; send null to clear it.

integer format: int64
nullable
Examplegenerated
{
"occurrenceDate": "example",
"scheduledPaymentDate": "example",
"amountCents": 1,
"note": "example",
"skip": true,
"forecastDate": "example",
"forecastAmountCents": 1
}

Marked

Media typeapplication/json
object
data
required
object
billId
string
occurrenceDate

Calendar date, ‘YYYY-MM-DD’

string
scheduledMark
object
scheduledPaymentDate
required

Calendar date, ‘YYYY-MM-DD’

string
amountCents
required

Integer minor units (cents)

integer format: int64
note
string
nullable
skipped
boolean
forecastDate

The FORECAST-ONLY date override on this occurrence, or null (KAN-2926). ALWAYS emitted. Outranked by scheduledMark when both are present. It moves presentation and the projected step only — never month bucketing, window membership or Safe to Spend.

string
nullable
forecastAmountCents

The FORECAST-ONLY amount override, as the POSITIVE MAGNITUDE it is stored as, or null (KAN-2926). ALWAYS emitted. The signed figure is effectiveAmountCents; never re-sign this on the client. Outranked by scheduledMark when both are present.

integer format: int64
nullable
settledAgainst
object
transactionId
required
string format: uuid
accountId
required
string format: uuid
transactionDate
required

The bank row’s POSTED date, ‘YYYY-MM-DD’.

string
amountCents
required

The bank row’s SIGNED ledger amount as stored, integer minor units (cents).

integer format: int64
message
string
requestId
required
string
timestamp
required
string format: date-time
Examplegenerated
{
"data": {
"billId": "example",
"occurrenceDate": "example",
"scheduledMark": {
"scheduledPaymentDate": "example",
"amountCents": 1
},
"note": "example",
"skipped": true,
"forecastDate": "example",
"forecastAmountCents": 1,
"settledAgainst": {
"transactionId": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0",
"accountId": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0",
"transactionDate": "example",
"amountCents": 1
}
},
"message": "example",
"requestId": "example",
"timestamp": "2026-04-15T12:00:00Z"
}

Validation failed — see errorCode / fieldErrors

Media typeapplication/json
object
errorCode
required

Machine-readable error code (AUTH_ERROR_CODES).

string
Allowed values: INVALID_EMAIL_FORMAT USER_ALREADY_EXISTS INVALID_INVITE_CODE WRONG_PASSWORD USER_NOT_FOUND AUTHENTICATION_FAILED EXPIRED_CODE INVALID_CODE INVALID_PASSWORD VALIDATION_ERROR SIGNUP_FAILED DATABASE_ERROR SERVICE_UNAVAILABLE PLAID_ERROR IDEMPOTENCY_CONFLICT ACCOUNT_ALREADY_LINKED NOT_FOUND
message
required
string
fieldErrors

Optional field-level validation errors, keyed by field name.

object
key
additional properties
Array<string>
requestId
required
string
timestamp
required
string format: date-time
Example
{
"errorCode": "INVALID_EMAIL_FORMAT"
}

Missing, invalid, or expired bearer token

Media typeapplication/json
object
errorCode
required

Machine-readable error code (AUTH_ERROR_CODES).

string
Allowed values: INVALID_EMAIL_FORMAT USER_ALREADY_EXISTS INVALID_INVITE_CODE WRONG_PASSWORD USER_NOT_FOUND AUTHENTICATION_FAILED EXPIRED_CODE INVALID_CODE INVALID_PASSWORD VALIDATION_ERROR SIGNUP_FAILED DATABASE_ERROR SERVICE_UNAVAILABLE PLAID_ERROR IDEMPOTENCY_CONFLICT ACCOUNT_ALREADY_LINKED NOT_FOUND
message
required
string
fieldErrors

Optional field-level validation errors, keyed by field name.

object
key
additional properties
Array<string>
requestId
required
string
timestamp
required
string format: date-time
Example
{
"errorCode": "INVALID_EMAIL_FORMAT"
}

Authenticated, but the tenant gate refuses the request until the caller resolves a precondition. errorCode is one of EMAIL_NOT_VERIFIED, POLICY_ACCEPTANCE_REQUIRED, or SUBSCRIPTION_REQUIRED, evaluated in exactly that order (contract term CCR-1: email verification first, then policy acceptance, then subscription — so a subscriber who has merely not re-accepted the current policies always sees POLICY_ACCEPTANCE_REQUIRED). A POLICY_ACCEPTANCE_REQUIRED body additionally carries details.outstanding (the policy versions still to accept) and details.firstAcceptance. The SUBSCRIPTION_REQUIRED arm is INERT unless the server-side BILLING_ENABLED flag is exactly the string true; while it is off, only the first two codes are reachable. Not retryable as sent — resolve the named condition, then resend.

Media typeapplication/json
object
errorCode
required

Which precondition refused the request. Evaluated in this order (CCR-1); SUBSCRIPTION_REQUIRED is unreachable while BILLING_ENABLED is not exactly true.

string
Allowed values: EMAIL_NOT_VERIFIED POLICY_ACCEPTANCE_REQUIRED SUBSCRIPTION_REQUIRED
message
required
string
details
object
outstanding
required

Active policy versions the user has not yet accepted.

Array<object>
object
id
required
string format: uuid
policyType
required
string
version
required
integer
title
required
string
firstAcceptance
required

True when the user has accepted no policy before — the client renders the new-signup screen rather than the re-acceptance one.

boolean
requestId
required
string
timestamp
required
string format: date-time
Example
{
"errorCode": "EMAIL_NOT_VERIFIED"
}

Resource not found or not owned by the caller org

Media typeapplication/json
object
errorCode
required

Machine-readable error code (AUTH_ERROR_CODES).

string
Allowed values: INVALID_EMAIL_FORMAT USER_ALREADY_EXISTS INVALID_INVITE_CODE WRONG_PASSWORD USER_NOT_FOUND AUTHENTICATION_FAILED EXPIRED_CODE INVALID_CODE INVALID_PASSWORD VALIDATION_ERROR SIGNUP_FAILED DATABASE_ERROR SERVICE_UNAVAILABLE PLAID_ERROR IDEMPOTENCY_CONFLICT ACCOUNT_ALREADY_LINKED NOT_FOUND
message
required
string
fieldErrors

Optional field-level validation errors, keyed by field name.

object
key
additional properties
Array<string>
requestId
required
string
timestamp
required
string format: date-time
Example
{
"errorCode": "INVALID_EMAIL_FORMAT"
}

Conflict. errorCode is IDEMPOTENCY_CONFLICT when an X-Idempotency-Key was replayed with a different request payload; where the operation instead detects a resource-state conflict it carries that endpoint’s own errorCode (a duplicate name, an import that is no longer committable, an account already linked to a bank connection).

Media typeapplication/json
object
errorCode
required

Machine-readable error code (AUTH_ERROR_CODES).

string
Allowed values: INVALID_EMAIL_FORMAT USER_ALREADY_EXISTS INVALID_INVITE_CODE WRONG_PASSWORD USER_NOT_FOUND AUTHENTICATION_FAILED EXPIRED_CODE INVALID_CODE INVALID_PASSWORD VALIDATION_ERROR SIGNUP_FAILED DATABASE_ERROR SERVICE_UNAVAILABLE PLAID_ERROR IDEMPOTENCY_CONFLICT ACCOUNT_ALREADY_LINKED NOT_FOUND
message
required
string
fieldErrors

Optional field-level validation errors, keyed by field name.

object
key
additional properties
Array<string>
requestId
required
string
timestamp
required
string format: date-time
Example
{
"errorCode": "INVALID_EMAIL_FORMAT"
}

Internal error (no internal detail leaked)

Media typeapplication/json
object
errorCode
required

Machine-readable error code (AUTH_ERROR_CODES).

string
Allowed values: INVALID_EMAIL_FORMAT USER_ALREADY_EXISTS INVALID_INVITE_CODE WRONG_PASSWORD USER_NOT_FOUND AUTHENTICATION_FAILED EXPIRED_CODE INVALID_CODE INVALID_PASSWORD VALIDATION_ERROR SIGNUP_FAILED DATABASE_ERROR SERVICE_UNAVAILABLE PLAID_ERROR IDEMPOTENCY_CONFLICT ACCOUNT_ALREADY_LINKED NOT_FOUND
message
required
string
fieldErrors

Optional field-level validation errors, keyed by field name.

object
key
additional properties
Array<string>
requestId
required
string
timestamp
required
string format: date-time
Example
{
"errorCode": "INVALID_EMAIL_FORMAT"
}

Service temporarily unavailable — errorCode is SERVICE_UNAVAILABLE. A TRANSIENT, RETRYABLE condition rather than a defect in the request: a query that exceeded its time budget, a lost or refused database connection, a saturated connection pool, or an outage at an upstream provider the request depends on (the authentication provider, or the bank data provider on a bank-connection operation). The identical request may succeed on retry — back off briefly and, on a write, resend the same X-Idempotency-Key where the operation accepts one.

Media typeapplication/json
object
errorCode
required

Machine-readable error code (AUTH_ERROR_CODES).

string
Allowed values: INVALID_EMAIL_FORMAT USER_ALREADY_EXISTS INVALID_INVITE_CODE WRONG_PASSWORD USER_NOT_FOUND AUTHENTICATION_FAILED EXPIRED_CODE INVALID_CODE INVALID_PASSWORD VALIDATION_ERROR SIGNUP_FAILED DATABASE_ERROR SERVICE_UNAVAILABLE PLAID_ERROR IDEMPOTENCY_CONFLICT ACCOUNT_ALREADY_LINKED NOT_FOUND
message
required
string
fieldErrors

Optional field-level validation errors, keyed by field name.

object
key
additional properties
Array<string>
requestId
required
string
timestamp
required
string format: date-time
Example
{
"errorCode": "INVALID_EMAIL_FORMAT"
}