# `BankingCircle.Payments`
[🔗](https://github.com/iamkanishka/banking_circle/blob/main/lib/banking_circle/payments.ex#L1)

Single and bulk payment initiation, status tracking, cancellation, and
lookup, per the Payment Lifecycle documentation.

Every function accepts a client name (defaults to `:default`) as its
last argument, matching the multi-client design in
`BankingCircle.Config`.

## Idempotency caveat

Only Direct Debit Collections (`BankingCircle.DirectDebit`) carries
documented idempotency-key support at time of writing. `create_single/2`
and `create_bulk/2` do **not** — if a request times out, check
`get_by_transaction_reference/2` (or `get_bulk/2`) before blindly
resubmitting, to avoid a duplicate payment. See the moduledoc on
`BankingCircle.HTTP.Middleware.Retry` for how this shapes our retry
policy: neither of these POSTs is auto-retried on a 5xx/429.

# `client`

```elixir
@type client() :: atom()
```

# `payment_id`

```elixir
@type payment_id() :: String.t()
```

# `cancel_bulk`

```elixir
@spec cancel_bulk(String.t(), client()) ::
  {:ok, map()} | {:error, BankingCircle.Error.t()}
```

Attempts to cancel every cancellable payment in a batch (same
pre-processing rule as `cancel_single/2`). Supports partial cancellation
— returns a summary of which rows succeeded.

# `cancel_single`

```elixir
@spec cancel_single(payment_id(), client()) ::
  {:ok, map()} | {:error, BankingCircle.Error.t()}
```

Cancels a payment. Only succeeds if the payment hasn't already entered
processing — returns `{:error, %BankingCircle.Error{kind: :client_error}}`
if it's too late.

# `cancel_trace`

```elixir
@spec cancel_trace(String.t(), client()) ::
  {:ok, map()} | {:error, BankingCircle.Error.t()}
```

Cancels a trace request while it's still in `Processing` status.

# `create_bulk`

```elixir
@spec create_bulk([map()], client()) ::
  {:ok, map()} | {:error, BankingCircle.Error.t() | [{pos_integer(), term()}]}
```

Submits a batch of payments as a single bulk request. `rows` is a list
of attribute maps, each validated the same way as `create_single/2`; if
any row is invalid the whole batch is rejected client-side with
`{:error, [{index, changeset}, ...]}` before any network call, using
1-based indices matching Banking Circle's `elementIndex` error semantics.

# `create_fi_to_fi`

```elixir
@spec create_fi_to_fi(map(), client()) ::
  {:ok, map()} | {:error, BankingCircle.Error.t()}
```

Initiates a Correspondent or Agency Banking payment from your own BIC
and IBANs (`POST /api/v1/payments/fi-to-fi-customer-credit-transfer-initiation`).
Requires special permissions on your API user — contact your
Relationship Manager if you get an authorization error here.

This is a materially richer payload than a single payment: full
structured debtor/creditor addresses, and — for the "on behalf of"
scenario — separate debtor vs. **ultimate** debtor fields (provide
either; if both, the ultimate debtor is carried forward as the debtor
name on the wire). Given the shape variance we don't run this through
the `BankingCircle.Schemas.Payment` changeset — pass the raw
API-shaped-but-snake_case attrs and we camelCase top-level keys for you;
nested structures (addresses, amount) should already be maps matching
the API's field names.

## FX

Three ways to convert currency on an outbound Correspondent/Agency
payment, combining this endpoint with `BankingCircle.FX`:

  * **Embedded/live rate** — set `currency_of_transfer` different from
    `amount.currency`; conversion happens automatically at the live rate.
  * **RFQ** — get a quote via `BankingCircle.FX.request_quotes/2`
    (30-second lock), then pass its id as `fx_quote_id`.
  * **Held rate** — get one via `BankingCircle.FX.held_rate/5`, then
    pass its id as `fx_quote_id`.

# `create_single`

```elixir
@spec create_single(map(), client()) ::
  {:ok, map()} | {:error, BankingCircle.Error.t() | Ecto.Changeset.t()}
```

Submits a single payment. `attrs` is validated via
`BankingCircle.Schemas.Payment.changeset/1` before it ever reaches the
network — invalid input returns `{:error, %Ecto.Changeset{}}` without a
request being made.

On success, returns `{:ok, %{"id" => payment_id, ...}}`.

# `get_bulk`

```elixir
@spec get_bulk(String.t(), client()) ::
  {:ok, map()} | {:error, BankingCircle.Error.t()}
```

Detailed stats for a bulk batch: overall status, per-status counts,
total payment count, aggregated amount.

# `get_by_transaction_reference`

```elixir
@spec get_by_transaction_reference(String.t(), client()) ::
  {:ok, map()} | {:error, BankingCircle.Error.t()}
```

Looks up a single payment by its caller-assigned transaction reference.

# `get_mt103`

```elixir
@spec get_mt103(payment_id(), client()) ::
  {:ok, binary()} | {:error, BankingCircle.Error.t()}
```

Fetches the MT103 confirmation message for a completed payment.

# `get_recall`

```elixir
@spec get_recall(String.t(), client()) ::
  {:ok, map()} | {:error, BankingCircle.Error.t()}
```

# `get_recall_status`

```elixir
@spec get_recall_status(String.t(), client()) ::
  {:ok, map()} | {:error, BankingCircle.Error.t()}
```

Lightweight status-only check for a recall — cheaper than `get_recall/2` when you just need to know Received/Successful/Unsuccessful.

# `get_single`

```elixir
@spec get_single(payment_id(), client()) ::
  {:ok, map()} | {:error, BankingCircle.Error.t()}
```

Full payment detail, including processing history.

# `get_status`

```elixir
@spec get_status(payment_id(), client()) ::
  {:ok, map()} | {:error, BankingCircle.Error.t()}
```

Lightweight status check — intended for polling, cheaper than `get_single/2`.

# `get_trace`

```elixir
@spec get_trace(String.t(), client()) ::
  {:ok, map()} | {:error, BankingCircle.Error.t()}
```

# `get_trace_status`

```elixir
@spec get_trace_status(String.t(), client()) ::
  {:ok, map()} | {:error, BankingCircle.Error.t()}
```

# `initiate_recall`

```elixir
@spec initiate_recall(String.t(), String.t(), client()) ::
  {:ok, map()} | {:error, BankingCircle.Error.t()}
```

Requests a recall of a processed payment (you sent it to the wrong
place, or with wrong details). `reason_code` must be one of `"AC03"`
(invalid creditor account), `"AM09"` (wrong amount), or `"CUST"`
(requested by customer).

Requires the payment to be processed, under 6 months old, through a
non-instant scheme (Faster Payments/SEPA Instant recalls go straight to
`Unsuccessful`), and without an existing active/successful recall.
Banking Circle makes up to 3 attempts (initial + 2 chasers, 5 working
days apart) — there's no cancel-a-recall operation; contact Client
Services if you need one stopped.

# `initiate_trace`

```elixir
@spec initiate_trace(String.t(), client()) ::
  {:ok, map()} | {:error, BankingCircle.Error.t()}
```

Initiates a trace to investigate a delayed/missing payment by contacting
the beneficiary bank. Requires the payment to be in a final status
(Processed or Rejected), under 6 months old, without an existing active
trace. Faster Payments/SEPA Instant traces go straight to `Unsuccessful`.

# `list_bulks`

```elixir
@spec list_bulks(
  keyword(),
  client()
) :: {:ok, map()} | {:error, BankingCircle.Error.t()}
```

Paginated list of bulk payment batches.

# `list_recalls`

```elixir
@spec list_recalls(
  keyword(),
  client()
) :: {:ok, map()} | {:error, BankingCircle.Error.t()}
```

Lists recall requests from the past 6 months, paginated.

# `list_singles`

```elixir
@spec list_singles(
  keyword(),
  client()
) :: {:ok, map()} | {:error, BankingCircle.Error.t()}
```

Paginated list of single payments, optionally filtered by `status`/date range.

# `list_traces`

```elixir
@spec list_traces(
  keyword(),
  client()
) :: {:ok, map()} | {:error, BankingCircle.Error.t()}
```

# `reject_single`

```elixir
@spec reject_single(payment_id(), String.t() | nil, client()) ::
  {:ok, map()} | {:error, BankingCircle.Error.t()}
```

Rejects an incoming direct debit payment.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
