Strong Customer Authentication (SCA) / Change Request completion flow.
Many Solaris write operations (SEPA transfers, person updates, adding trusted IBANs,
etc.) require customer authentication. When triggered, the API returns
202 Accepted with a change_request_id in the body. The customer must
authorise the action before it is executed.
Authentication Methods
SMS OTP — Sends a one-time password to the customer's verified mobile number:
POST /change_requests/{id}/authorize/otp # send SMS
POST /change_requests/{id}/confirm/otp # submit codeDevice Signing — Uses the Solaris mobile SDK on the customer's device:
GET /change_requests/{id}/challenge # get challenge string
POST /change_requests/{id}/confirm/device # submit signed tokenFull Flow
# 1. Initiate any action (e.g. SEPA transfer)
{:ok, result} = Solaris.Banking.SEPA.create_person_credit_transfer(...)
change_request_id = result["change_request_id"]
# 2a. SMS OTP path
{:ok, _} = Solaris.ChangeRequests.authorize_with_sms(change_request_id)
# ... customer receives SMS and submits OTP ...
{:ok, _} = Solaris.ChangeRequests.confirm_with_otp(change_request_id, "483920")
# 2b. Device signing path
{:ok, challenge} = Solaris.ChangeRequests.get_device_challenge(change_request_id)
# ... pass challenge["challenge"] to mobile SDK, get signed_token back ...
{:ok, _} = Solaris.ChangeRequests.confirm_with_device(change_request_id, signed_token)Statuses
| Status | Description |
|---|---|
AUTHORIZATION_REQUIRED | Awaiting customer auth |
COMPLETED | Authorised and executed |
FAILED | Authentication failed |
EXPIRED | Challenge window expired |
Summary
Functions
Initiates an SMS OTP challenge for a change request.
Confirms a change request using a device-signed token.
Confirms a change request using the SMS OTP code.
Retrieves a change request by ID.
Retrieves the device signing challenge string for a change request.
Lists change requests for a business.
Lists change requests for a person.
Polls a change request until it reaches a terminal status.
Functions
@spec authorize_with_sms( String.t(), keyword() ) :: {:ok, map()} | {:error, Solaris.Error.t()}
Initiates an SMS OTP challenge for a change request.
Sends a one-time password to the customer's registered and verified
mobile number. The number must have been confirmed via
Solaris.Onboarding.Persons.confirm_mobile_number/3 before use.
@spec confirm_with_device(String.t(), String.t(), keyword()) :: {:ok, map()} | {:error, Solaris.Error.t()}
Confirms a change request using a device-signed token.
The signed_token is produced by the Solaris mobile SDK after the customer
authenticates on their enrolled device.
@spec confirm_with_otp(String.t(), String.t(), keyword()) :: {:ok, map()} | {:error, Solaris.Error.t()}
Confirms a change request using the SMS OTP code.
Returns {:error, %Error{code: :forbidden}} if the OTP is invalid or expired.
Examples
{:ok, _} = Solaris.ChangeRequests.confirm_with_otp("cchr_123", "483920")
@spec get( String.t(), keyword() ) :: {:ok, map()} | {:error, Solaris.Error.t()}
Retrieves a change request by ID.
Examples
{:ok, cr} = Solaris.ChangeRequests.get("cchr_123")
cr["status"] # => "AUTHORIZATION_REQUIRED"
@spec get_device_challenge( String.t(), keyword() ) :: {:ok, map()} | {:error, Solaris.Error.t()}
Retrieves the device signing challenge string for a change request.
Pass the returned challenge["challenge"] value to the Solaris mobile SDK
for signing on the customer's enrolled device.
@spec list_for_business( String.t(), keyword() ) :: {:ok, map()} | {:error, Solaris.Error.t()}
Lists change requests for a business.
@spec list_for_person( String.t(), keyword() ) :: {:ok, map()} | {:error, Solaris.Error.t()}
Lists change requests for a person.
@spec poll_until_complete( String.t(), keyword() ) :: {:ok, map()} | {:error, Solaris.Error.t() | :timeout}
Polls a change request until it reaches a terminal status.
Useful for synchronous test flows. Not recommended in production — prefer listening to the relevant webhook instead.
Options
| Option | Default | Description |
|---|---|---|
:interval_ms | 2_000 | Polling interval in milliseconds |
:max_attempts | 15 | Maximum number of poll attempts |
:terminal_statuses | ["COMPLETED", "FAILED", "EXPIRED"] | Stop conditions |
Examples
{:ok, completed} = Solaris.ChangeRequests.poll_until_complete("cchr_123")
completed["status"] # => "COMPLETED"
# Returns {:error, :timeout} if never completed within max_attempts