create_transfers
Create one or more Transfer
s. A successfully created transfer will modify the
amount fields of its debit and
credit accounts.
Event
The transfer to create. See Transfer
for constraints.
Result
Results are listed in this section in order of descending precedence — that is, if more than one error is applicable to the transfer being created, only the result listed first is returned.
ok
The transfer was successfully created; did not previously exist.
Note that ok
is generated by the client implementation; the network protocol does not include a
result when the transfer was successfully created.
linked_event_failed
The transfer was not created. One or more of the other transfers in the linked chain is invalid, so the whole chain failed.
linked_event_chain_open
The transfer was not created. The Transfer.flags.linked
flag was set
on the last event in the batch, which is not legal. (flags.linked
indicates that the chain
continues to the next operation).
imported_event_expected
The transfer was not created. The Transfer.flags.imported
was
set on the first transfer of the batch, but not all transfers in the batch.
Batches cannot mix imported transfers with non-imported transfers.
imported_event_not_expected
The transfer was not created. The Transfer.flags.imported
was
expected to not be set, as it's not allowed to mix transfers with different imported
flag
in the same batch. The first transfer determines the entire operation.
timestamp_must_be_zero
This result only applies when Account.flags.imported
is not set.
The transfer was not created. The Transfer.timestamp
is nonzero, but
must be zero. The cluster is responsible for setting this field.
The Transfer.timestamp
can only be assigned when creating transfers
with Transfer.flags.imported
set.
imported_event_timestamp_out_of_range
This result only applies when Transfer.flags.imported
is set.
The transfer was not created. The Transfer.timestamp
is out of range,
but must be a user-defined timestamp greater than 0
and less than 2^63
.
imported_event_timestamp_must_not_advance
This result only applies when Transfer.flags.imported
is set.
The transfer was not created. The user-defined Transfer.timestamp
is
greater than the current cluster time, but it must be a past timestamp.
reserved_flag
The transfer was not created. Transfer.flags.reserved
is nonzero, but must be zero.
id_must_not_be_zero
The transfer was not created. Transfer.id
is zero, which is a reserved value.
id_must_not_be_int_max
The transfer was not created. Transfer.id
is 2^128 - 1
, which is a reserved
value.
exists_with_different_flags
A transfer with the same id
already exists, but with different flags
.
exists_with_different_pending_id
A transfer with the same id
already exists, but with a different
pending_id
.
exists_with_different_timeout
A transfer with the same id
already exists, but with a different
timeout
.
exists_with_different_debit_account_id
A transfer with the same id
already exists, but with a different
debit_account_id
.
exists_with_different_credit_account_id
A transfer with the same id
already exists, but with a different
credit_account_id
.
exists_with_different_amount
A transfer with the same id
already exists, but with a different
amount
.
If the transfer has flags.balancing_debit
or
flags.balancing_credit
set, then the actual amount
transferred exceeds this failed transfer's amount
.
exists_with_different_user_data_128
A transfer with the same id
already exists, but with a different
user_data_128
.
exists_with_different_user_data_64
A transfer with the same id
already exists, but with a different
user_data_64
.
exists_with_different_user_data_32
A transfer with the same id
already exists, but with a different
user_data_32
.
exists_with_different_ledger
A transfer with the same id
already exists, but with a different ledger
.
exists_with_different_code
A transfer with the same id
already exists, but with a different code
.
exists
A transfer with the same id
already exists.
If the transfer has flags.balancing_debit
or
flags.balancing_credit
set, then the existing
transfer may have a different amount
, limited to the maximum
amount
of the transfer in the request.
If the transfer has flags.post_pending_transfer
set, then the existing transfer may have a different amount
:
- If the original posted amount was less than the pending amount, then the transfer amount must be equal to the posted amount.
- Otherwise, the transfer amount must be greater than or equal to the pending amount.
Client release < 0.16.0
If the transfer has flags.balancing_debit
or
flags.balancing_credit
set, then the existing
transfer may have a different amount
, limited to the maximum
amount
of the transfer in the request.
Otherwise, with the possible exception of the timestamp
field, the existing transfer is identical
to the transfer in the request.
To correctly recover from application crashes,
many applications should handle exists
exactly as ok
.
id_already_failed
The transfer was not created. A previous transfer with the same id
failed
due to one of the following transient errors:
debit_account_not_found
credit_account_not_found
pending_transfer_not_found
exceeds_credits
exceeds_debits
debit_account_already_closed
credit_account_already_closed
Transient errors depend on the database state at a given point in time, and each attempt
is uniquely associated with the corresponding Transfer.id
.
This behavior guarantees that retrying a transfer will not produce a different outcome
(either success or failure).
Without this mechanism, a transfer that previously failed could succeed if retried when the underlying state changes (e.g., the target account has sufficient credits).
Note: The application should retry an event only if it was unable to acknowledge the last response (e.g., due to an application restart) or because it is correcting a previously rejected malformed request (e.g., due to an application bug). If the application intends to submit the transfer again even after a transient error, it must generate a new idempotency id.
Client release < 0.16.4
The id
is never checked against failed transfers, regardless of the error.
Therefore, a transfer that failed due to a transient error could succeed if retried later.
flags_are_mutually_exclusive
The transfer was not created. A transfer cannot be created with the specified combination of
Transfer.flags
.
Flag compatibility (✓ = compatible, ✗ = mutually exclusive):
flags.pending
flags.post_pending_transfer
flags.void_pending_transfer
flags.balancing_debit
flags.balancing_credit
flags.closing_debit
flags.closing_credit
flags.imported
debit_account_id_must_not_be_zero
The transfer was not created. Transfer.debit_account_id
is
zero, but must be a valid account id.
debit_account_id_must_not_be_int_max
The transfer was not created. Transfer.debit_account_id
is
2^128 - 1
, but must be a valid account id.
credit_account_id_must_not_be_zero
The transfer was not created. Transfer.credit_account_id
is
zero, but must be a valid account id.
credit_account_id_must_not_be_int_max
The transfer was not created. Transfer.credit_account_id
is
2^128 - 1
, but must be a valid account id.
accounts_must_be_different
The transfer was not created. Transfer.debit_account_id
and
Transfer.credit_account_id
must not be equal.
That is, an account cannot transfer money to itself.
pending_id_must_be_zero
The transfer was not created. Only post/void transfers can reference a pending transfer.
Either:
Transfer.flags.post_pending_transfer
must be set, orTransfer.flags.void_pending_transfer
must be set, orTransfer.pending_id
must be zero.
pending_id_must_not_be_zero
The transfer was not created.
Transfer.flags.post_pending_transfer
or
Transfer.flags.void_pending_transfer
is set, but
Transfer.pending_id
is zero. A posting or voiding transfer must
reference a pending
transfer.
pending_id_must_not_be_int_max
The transfer was not created. Transfer.pending_id
is 2^128 - 1
,
which is a reserved value.
pending_id_must_be_different
The transfer was not created. Transfer.pending_id
is set to the same
id as Transfer.id
. Instead it should refer to a different (existing)
transfer.
timeout_reserved_for_pending_transfer
The transfer was not created. Transfer.timeout
is nonzero, but only
pending transfers have nonzero timeouts.
closing_transfer_must_be_pending
The transfer was not created. Transfer.flags.pending
is not set,
but closing transfers must be two-phase pending transfers.
If either Transfer.flags.closing_debit
or
Transfer.flags.closing_credit
is set,
Transfer.flags.pending
must also be set.
This ensures that closing transfers are reversible by voiding the pending transfer, and requires that the reversal operation references the corresponding closing transfer, guarding against unexpected interleaving of close/unclose operations.
amount_must_not_be_zero
Deprecated: This error code is only returned to clients prior to release 0.16.0
.
Since 0.16.0
, zero-amount transfers are permitted.
Client release < 0.16.0
The transfer was not created. Transfer.amount
is zero, but must be
nonzero.
Every transfer must move value. Only posting and voiding transfer amounts may be zero — when zero, they will move the full pending amount.
ledger_must_not_be_zero
The transfer was not created. Transfer.ledger
is zero, but must be
nonzero.
code_must_not_be_zero
The transfer was not created. Transfer.code
is zero, but must be nonzero.
debit_account_not_found
The transfer was not created. Transfer.debit_account_id
must
refer to an existing Account
.
This is a transient error.
The Transfer.id
associated with this particular attempt will always fail
upon retry, even if the underlying issue is resolved.
To succeed, a new idempotency id must be submitted.
credit_account_not_found
The transfer was not created. Transfer.credit_account_id
must
refer to an existing Account
.
This is a transient error.
The Transfer.id
associated with this particular attempt will always fail
upon retry, even if the underlying issue is resolved.
To succeed, a new idempotency id must be submitted.
accounts_must_have_the_same_ledger
The transfer was not created. The accounts referred to by
Transfer.debit_account_id
and
Transfer.credit_account_id
must have an identical
ledger
.
Currency exchange is implemented with multiple transfers.
transfer_must_have_the_same_ledger_as_accounts
The transfer was not created. The accounts referred to by
Transfer.debit_account_id
and
Transfer.credit_account_id
are equivalent, but differ from the
Transfer.ledger
.
pending_transfer_not_found
The transfer was not created. The transfer referenced by
Transfer.pending_id
does not exist.
This is a transient error.
The Transfer.id
associated with this particular attempt will always fail
upon retry, even if the underlying issue is resolved.
To succeed, a new idempotency id must be submitted.
pending_transfer_not_pending
The transfer was not created. The transfer referenced by
Transfer.pending_id
exists, but does not have
flags.pending
set.
pending_transfer_has_different_debit_account_id
The transfer was not created. The transfer referenced by
Transfer.pending_id
exists, but with a different
debit_account_id
.
The post/void transfer's debit_account_id
must either be 0
or identical to the pending
transfer's debit_account_id
.
pending_transfer_has_different_credit_account_id
The transfer was not created. The transfer referenced by
Transfer.pending_id
exists, but with a different
credit_account_id
.
The post/void transfer's credit_account_id
must either be 0
or identical to the pending
transfer's credit_account_id
.
pending_transfer_has_different_ledger
The transfer was not created. The transfer referenced by
Transfer.pending_id
exists, but with a different
ledger
.
The post/void transfer's ledger
must either be 0
or identical to the pending transfer's
ledger
.
pending_transfer_has_different_code
The transfer was not created. The transfer referenced by
Transfer.pending_id
exists, but with a different
code
.
The post/void transfer's code
must either be 0
or identical to the pending transfer's code
.
exceeds_pending_transfer_amount
The transfer was not created. The transfer's amount
exceeds the amount
of its pending transfer.
pending_transfer_has_different_amount
The transfer was not created. The transfer is attempting to
void a pending transfer. The voiding transfer's
amount
must be either 0
or exactly the amount
of the pending
transfer.
To partially void a transfer, create a posting transfer
with an amount less than the pending transfer's amount
.
Client release < 0.16.0
To partially void a transfer, create a posting transfer
with an amount between 0
and the pending transfer's amount
.
pending_transfer_already_posted
The transfer was not created. The referenced pending transfer was
already posted by a post_pending_transfer
.
pending_transfer_already_voided
The transfer was not created. The referenced pending transfer was
already voided by a void_pending_transfer
.
pending_transfer_expired
The transfer was not created. The referenced pending transfer was already voided because its timeout has passed.
imported_event_timestamp_must_not_regress
This result only applies when Transfer.flags.imported
is set.
The transfer was not created. The user-defined Transfer.timestamp
regressed, but it must be greater than the last timestamp assigned to any Transfer
in the cluster and cannot be equal to the timestamp of any existing Account
.
imported_event_timestamp_must_postdate_debit_account
This result only applies when Transfer.flags.imported
is set.
The transfer was not created. Transfer.debit_account_id
must
refer to an Account
whose timestamp
is less than the
Transfer.timestamp
.
imported_event_timestamp_must_postdate_credit_account
This result only applies when Transfer.flags.imported
is set.
The transfer was not created. Transfer.credit_account_id
must
refer to an Account
whose timestamp
is less than the
Transfer.timestamp
.
imported_event_timeout_must_be_zero
This result only applies when Transfer.flags.imported
is set.
The transfer was not created. The Transfer.timeout
is nonzero, but
must be zero.
It's possible to import pending transfers with a user-defined timestamp, but since it's not driven by the cluster clock, it cannot define a timeout for automatic expiration. In those cases, the two-phase post or rollback must be done manually.
debit_account_already_closed
The transfer was not created. Transfer.debit_account_id
must
refer to an Account
whose Account.flags.closed
is not already set.
This is a transient error.
The Transfer.id
associated with this particular attempt will always fail
upon retry, even if the underlying issue is resolved.
To succeed, a new idempotency id must be submitted.
credit_account_already_closed
The transfer was not created. Transfer.credit_account_id
must
refer to an Account
whose Account.flags.closed
is not already set.
This is a transient error.
The Transfer.id
associated with this particular attempt will always fail
upon retry, even if the underlying issue is resolved.
To succeed, a new idempotency id must be submitted.
overflows_debits_pending
The transfer was not created. debit_account.debits_pending + transfer.amount
would overflow a
128-bit unsigned integer.
overflows_credits_pending
The transfer was not created. credit_account.credits_pending + transfer.amount
would overflow a
128-bit unsigned integer.
overflows_debits_posted
The transfer was not created. debit_account.debits_posted + transfer.amount
would overflow a
128-bit unsigned integer.
overflows_credits_posted
The transfer was not created. debit_account.credits_posted + transfer.amount
would overflow a
128-bit unsigned integer.
overflows_debits
The transfer was not created.
debit_account.debits_pending + debit_account.debits_posted + transfer.amount
would overflow a
128-bit unsigned integer.
overflows_credits
The transfer was not created.
credit_account.credits_pending + credit_account.credits_posted + transfer.amount
would overflow a
128-bit unsigned integer.
overflows_timeout
The transfer was not created. transfer.timestamp + (transfer.timeout * 1_000_000_000)
would
exceed 2^63
.
Transfer.timeout
is converted to nanoseconds.
This computation uses the Transfer.timestamp
value assigned by the
replica, not the 0
value sent by the client.
exceeds_credits
The transfer was not created.
The debit account has
flags.debits_must_not_exceed_credits
set, but
debit_account.debits_pending + debit_account.debits_posted + transfer.amount
would exceed
debit_account.credits_posted
.
This is a transient error.
The Transfer.id
associated with this particular attempt will always fail
upon retry, even if the underlying issue is resolved.
To succeed, a new idempotency id must be submitted.
Client release < 0.16.0
If flags.balancing_debit
is set, then
debit_account.debits_pending + debit_account.debits_posted + 1
would exceed
debit_account.credits_posted
.
exceeds_debits
The transfer was not created.
The credit account has
flags.credits_must_not_exceed_debits
set, but
credit_account.credits_pending + credit_account.credits_posted + transfer.amount
would exceed
credit_account.debits_posted
.
This is a transient error.
The Transfer.id
associated with this particular attempt will always fail
upon retry, even if the underlying issue is resolved.
To succeed, a new idempotency id must be submitted.
Client release < 0.16.0
If flags.balancing_credit
is set, then
credit_account.credits_pending + credit_account.credits_posted + 1
would exceed
credit_account.debits_posted
.
Client libraries
For language-specific docs see:
Internals
If you're curious and want to learn more, you can find the source code for creating a transfer in
src/state_machine.zig.
Search for fn create_transfer(
and fn execute(
.