Provides a virtual credit card that a user can use to make purchases.
Virtual cards are a core component of the Virtual Cards SDK. Virtual cards are used as a proxy between a user's personal Funding Source and online merchants.
In order to provision and use a virtual card, a Sudo and Funding Source are required.
Virtual cards make an important distinction between two subtypes: ProvisionalVirtualCard and VirtualCard.
A ProvisionalVirtualCard is a card that is currently being provisioned. This will typically exist in 3 different states: PROVISIONING, COMPLETED, and FAILED. It also contains a property card which contains the provisioned card, when it has COMPLETED. This type is transitory and should only be used for determining whether a card is provisioning, or has failed provisioning. Accessing provisioned cards should be done using methods that return Card types.
A VirtualCard is a card that has successfully finished provisioning. This is the card type that represents virtual cards.
Closed Cards
Cards will be closed and set to a CLOSED state under the following circumstances:
The card has expired; this occurs when your card reaches its expiry date.
The card has been closed via the cancel card API.
The Sudo owning the card has been deleted.
While provisional card records are transient, a card record is not. Cards will always be available for access via access methods even when closed. It is important to maintain a record of closed cards so that users can still examine the transaction history and also see any refunds that may still occur against the closed card from merchants where the card was used before the card was closed.
Orphan Cards
When a Sudo is deleted, any virtual cards owned by that Sudo will be closed and the entry for the Sudo in the owners property of any virtual cards owned by that Sudo will be removed. Such cards are called orphan cards and can be identified by looking for cards with no Sudo owner in the owners property. Sudo owners are identified as owners with issuer of value sudoplatform.sudoservice.
Provisioning a Virtual Card
A virtual card is provisioned by calling the provisionVirtualCard method.
An ownershipProofToken is required as part of the input. This ties together the Sudo and virtual card such that the Sudo becomes the owner of the virtual card. Use the getOwnershipProof method on the SudoProfilesClient in the SudoProfiles SDK in order to obtain an ownershipProofToken. See the Sudo section for more information.
The fundingSourceId is another required input parameter and is used to associate the virtual card with a funding source used to fund that particular virtual card.
Input properties cardHolder, billingAddress, and metadata are indicative of the user's information that they wish to be associated with the card.
cardHolder is the name that will appear on the card and is needed to perform transactions.
billingAddress is an optional property that the user can provide. If this is left undefined/nil/null, the billingAddress will be set by the platform itself.
metadata is a JSON object that contain application specific information about each virtual card. Examples of such information include an alias for the virtual card to help the user remember why they created it or perhaps a color for the virtual card to be rendered in by the application.
The currency is the three character ISO 4217 alphabetic currency code of the currency in which the virtual card is to be denominated.
Currently only USD is supported.
A call to the provisionVirtualCard method initiates the process of provisioning a virtual card and will return a ProvisionalCard on completion. During this process the ProvisionalCard traverses between a PROVISIONING and COMPLETED state.
To check the provisioning state after calling this API, calls to getProvisionalCard is used to poll for state changes of a card during its provisioning cycle. Once the ProvisionalCard has reached a COMPLETED state, the fully provisioned card can be accessed via the card property on the card (or via get/list of virtual cards).
try {constprovisionalCard=awaitvirtualCardsClient.provisionVirtualCard({// Refer to Sudo Profiles SDK for the information on how to obtain// a Sudo ownership proof. The audience parameter for the ownership// proof must be "sudoplatform.virtual-cards.virtual-card" to be// able to provision a virtual card for a Sudo. ownershipProofs: [ownershipProof], fundingSourceId:fundingSource.id, currency:'USD', cardHolder:'John Smith', billingAddress: { addressLine1:'123 Street Rd', addressLine2:undefined, city:'Salt Lake City', state:'UT', postalCode:'84044', country:'US', }, metadata: { alias:'Shopping', } })} catch (error) {// Handle/notify user of error.}
When provisioning a card for the first time, a key pair is generated on the Apple Keychain using the RSA algorithm. It is important to encourage a user to backup this key to ensure that they can access their data in the event that the key is lost. This key is linked to the keyNamespace property used when initializing a DefaultSudoVirtualCardsClient (for more info, see here).
The provisionCardWithInput method optionally takes observer parameter. To check the provisioning state after calling this API, calls to getVirtualCards(withLimit:) can be used, however the recommended way is to use a ProvisionCardObservable.
The ProvisionCardObservable is a protocol that facilitates the ability to track the state changes of a card during its provisioning cycle. This observer is required if expecting to gain access to the card once it has finished provisioning. Alternatively, you could also query a list of the user's cards to see if it has finished provisioning.
classProvisionVirtualCardViewController:UIViewController, ProvisionCardObserable {// ... Other view controller detailsfuncprovisioningStateDidChange(_state: ProvisionalCard.State, card: Card?) {switch state {case .provisioning:// Notify the user that their card is currently provisioningcase .completed:// if completed, card should not be niliflet card = card {// Handle success response } else {// Handle error }case .failed, .unknown:// Handle error } }funcerrorOccurred(_error: Error) {// Handle error }}
The virtual card can now be provisioned:
classProvisionVirtualCardViewController:UIViewController, ProvisionCardObserable {// ... Other view controller details// Ensure you retain a reference to a `DefaultSudoVirtualCardsClient` instance.let virtualCardsClient: SudoVirtualCardsClientfuncprovisionVirtualCard() async {do {// Retrieve the input details.let provisionInput =ProvisionCardInput( sudoId: sudo.id, fundingSourceId: fundingSource.id, cardHolder: input.cardHolder, billingAddress: input.billingAddress, currency: input.currency,/// Proof of Sudo ownership for provisioning cards. The ownership proof must/// contain an audience of "sudoplatform.virtual-cards.virtual-card" and can be/// obtained from SudoProfiles SDK via getOwnershipProof API. ownershipProof: ownershipProof, metadata: .dictionary(["alias": .string("Shopping") ]))let state =tryawait self.virtualCardsClient.provisionCard( withInput: provisionInput, observer: self) } catch {// Handle error } }}
A call to the provisionVirtualCard method initiates the process of provisioning a virtual card and will return a ProvisionalVirtualCard on completion. During this process the ProvisionalVirtualCard traverses between a PROVISIONING and COMPLETED state.
To check the provisioning state after calling this API, calls to getProvisionalVirtualCard is used to poll for state changes of a card during its provisioning cycle. Once the ProvisionalVirtualCard has reached a COMPLETED state, the fully provisioned card can be accessed.
// val ownershipProof: String// val fundingSource: FundingSourceval billingAddress =BillingAddress( addressLine1 ="123 Street Rd", addressLine2 =null, city ="Salt Lake City", state ="UT", postalCode ="84044", country ="US")val provisionInput =ProvisionVirtualCardInput(// Refer to Sudo Profiles SDK for the information on how to obtain// a Sudo ownership proof. The audience parameter for the ownership// proof must be "sudoplatform.virtual-cards.virtual-card" to be// able to provision a virtual card for a Sudo. ownershipProofs: [ownershipProof], fundingSourceId = fundingSource.id, cardHolder ="John Smith", alias ="Shopping", billingAddress = billingAddress, currency ="USD")launch {try {val initialProvisionalCard =withContext(Dispatchers.IO) { virtualCardsClient.provisionVirtualCard( provisionInput ) }var state = initialProvisionalCard.statevar virtualCard: VirtualCard? =nullwhile (state == ProvisionalCard.ProvisioningState.PROVISIONING) {val provisionalCard =withContext(Dispatchers.IO) { virtualCardsClient.getProvisionalCard(initialProvisionalCard.id) }if (provisionalCard?.state == ProvisionalCard.ProvisioningState.COMPLETED) { virtualCard = provisionalCard.cardbreak } state = provisionalCard?.state ?: ProvisionalCard.ProvisioningState.PROVISIONING }// The returned [virtualCard] has been created. } catch (e: VirtualCardException) {// Handle/notify user of exception }}
Updating a Virtual Card
The user may want to update details on their virtual card. The updateVirtualCard method is used to do this.
The modifiable fields of a virtual card are contained in the UpdateVirtualCardInput:
cardHolder
metadata
billingAddress
alias
These fields can all be updated independently.
The alias property is deprecated and replaced by the more general metadata property. To record an alias property for a card, set an alias property in the metadata JSON object.
A SingleAPIResult type is returned from this method call which contains the status of the update operation. Two possible statuses can be returned:
If you only want to update the alias to 'Banking' and the cardHolder to 'J Smith' then the API should be called like so:
The virtual card metadata property is a single attribute and all metadata properties must be specified on each update.
When updating a virtual card, you can supply either null, defined, or undefined for either updatable property. Supplying undefined will perform no update, supplying null will remove the value on the service (setting it to null), and supplying defined will update the content of the property. For example, if you have a card (userCard) that has the following properties:
To update the alias to "Banking", and billingAddress to null, call the update API:
do {let input =UpdateCardInput( id: userCard.id, cardHolder: userCard.cardHolder, billingAddress: .null, metadata: .defined( .dictionary(["alias": .string("Shopping") ])))let result =tryawait client.updateVirtualCard(withInput: input)switch result {case .success(let output):let updatedCard = output.result// `updatedCard` contains the updated card, with the previous details intact// and `alias` updated to "Banking".case .partial(let partial):let partialUpdatedCard = partial.resultlet error = partial.cause }} catch {// Handle error.
When updating a virtual card, ensure that all properties that are remaining unchanged are populated. For example, if you have a virtual card (userCard) that has the following properties:
If you only want to update the alias to "Banking", then the API should be called like so:
val input =UpdateVirtualCardInput( id = userCard.id, // card-id-1 cardHolder = userCard.cardHolder, // John Smith metadata = JsonValue.JsonString("Banking"), billingAddress = userCard.billingAddress // null)launch {try {val updateCard =withContext(Dispatchers.IO) { virtualCardsClient.updateVirtualCard(input) }when (updateCard) {is SingleAPIResult.Success -> {val card = updateCard.result// [card] contains the updated card, with the previous details intact// and [metadata] updated to "Banking". }is SingleAPIResult.Partial -> {val partialCard = updateCard.result.partialval error = updateCard.result.cause } } } catch (e: VirtualCardException) {// Handle/notify user of exception }}
Canceling a Virtual Card
A virtual card can be cancelled using the cancelVirtualCard method by passing in the id of an existing virtual card object.
This API call is idempotent, so any subsequent call to cancel with the same id will always yield the same result.
When cancelling a virtual card, the card will not be deleted, but instead will be transitioned to the CLOSED state and remain accessible via retrieval methods. Closed cards remain available so that transaction history can be retained and so that refunds that happen after the card is closed can be shown.
Once a virtual card is cancelled and its state is set to CLOSED, it cannot be reactivated - a new virtual card will need to be provisioned.
A SingleAPIResult type is returned from this method call which contains the status of the cancellation operation. Two possible statuses can be returned:
// Collect the input virtual card id however makes sense for your implementation.try {constresult=awaitvirtualCardsClient.cancelVirtualCard({ id:card.id })if (result.status ===APIResult.Success) {constcard=result.result } elseif (result.status ===APIResult.Partial) {constcardMetadata=result.itemconsterror=result.cause }} catch (error) {// Handle/notify error}
// Collect the input virtual card id however makes sense for your implementation.do {let result =tryawait self.virtualCardsClient.cancelVirtualCard(withId: id)switch result {case .success(let output):let card = output.resultcase .partial(let partial):let partialCard = partial.resultlet error = partial.cause} catch {// Handle/notify user of error}
// Collect the input virtual card id however makes sense for your implementation.// val virtualCardId = virtualCard.idlaunch {try {val cancelCard =withContext(Dispatchers.IO) { virtualCardsClient.cancelVirtualCard(virtualCardId) }when (cancelCard) {is SingleAPIResult.Success -> {val card = cancelCard.result }is SingleAPIResult.Partial -> {val partialCard = cancelCard.result.partialval error = cancelCard.result.cause } } } catch (e: VirtualCardException) {// Handle/notify user of exception }}
A fetch of single or multiple virtual cards can be performed remotely or locally by specifying the appropriate CachePolicy as part of the input.
Single Virtual Card by Id
To retrieve a single virtual card given its unique id, use the getVirtualCard method. This method will return the record if it exists.
try {constvirtualCard=awaitvirtualCardsClient.getVirtualCard({ id, cachePolicy:CachePolicy.RemoteOnly })// `virtual card` contains the virtual card object, else `undefined` if not found.} catch (error) {// Handle/notify error}
do {let virtualCard =tryawait self.virtualCardsClient.getVirtualCard( withId: id, cachePolicy: .remoteOnly)// If the id matches a virtual card, `virtualCard` will be returned, else nil.} catch {// Handle/notify user of error}
launch {try {val virtualCard =withContext(Dispatchers.IO) { virtualCardsClient.getVirtualCard( id ) }// If the [id] matches a virtual card, [virtualCard] will be returned, else [null] } catch (e: VirtualCardException) {// Handle/notify user of exception }}
Multiple Provisioned Virtual Cards
The ability to retrieve multiple or all virtual cards available to the user is supported. These results can be paginated and can contain virtual cards in various states.
A call to a list API will return a ListVirtualCardsResult with a status and depending on the status, a list of matching items and a nextToken to support pagination. If no results matching the input are found, the result will contain empty items. There can be three possible statuses returned:
A virtual card may fail to be unencrypted if the version of the client is not up-to-date or if the required cryptographic key is missing from the client device.
All Provisioned Virtual Cards
To retrieve multiple virtual cards that are owned by the signed in user, call the listVirtualCards method.
try {const result = await virtualCardsClient.listVirtualCards({ cachePolicy: CachePolicy.RemoteOnly, limit: 20, nextToken })if (result.status === ListOperationResultStatus.Success) {// `result.items` contains the list of items matching the input.// Page through the results if result.nextToken != undefined. } elseif (result.status === ListOperationResultStatus.Partial) {// `result.items` contains the list of items matching the input that decrypted successfully.// `result.failed` contains the list of items that failed decryption with associated error.// Page through the results if `partial.nextToken` != undefined. } else {// `result.cause` contains the error which caused the failure. }} catch (error) {// Handle/notify error}
do {let result =tryawait self.virtualCardsClient.listVirtualCards( withLimit:20, nextToken: nextToken, cachePolicy: .remoteOnly)switch result {case .success(let output):// `output.items` contains the list of items matching the input.// Page through the results if `output.nextToken` != nil.case .partial(let partial):// `partial.items` contains the list of items matching the input that decrypted successfully.// `partial.failed` contains the list of items that failed decryption with associated error.// Page through the results if `partial.nextToken` != nil.} catch {// Handle/notify user of error}
launch {try {val result =withContext(Dispatchers.IO) { virtualCardsClient.listVirtualCards( limit =20, nextToken = nextToken ) }when (result) {is ListAPIResult.Success -> {// [result.items] contains the list of items matching the input.// Page through the results if [output.nextToken] != null. }is ListAPIResult.Partial -> {// [result.items] contains the list of items matching the input that decrypted successfully.// [result.failed] contains the list of items that failed decryption with associated error.// Page through the results if [partial.nextToken] != null. } } } catch (e: VirtualCardException) {// Handle/notify user of exception }}