Skip to main content

Payment Provider

note

LINE IAP isn't handled by PaymentProvider

In-app purchase (IAP) for LINE MINI Apps isn't handled by the Unifi Apps SDK PaymentProvider, which only supports CRYPTO and STRIPE payments.

To offer IAP, implement it directly with the LIFF SDK (liff.iap.*). IAP is available only for verified LINE MINI Apps in Japan and requires IAP permission granted by LY within the MINI App channel.

For details, see LINE's [Integrate the in-app purchase feature] guide.
https://developers.line.biz/en/docs/line-mini-app/in-app-purchase/implement-in-app-purchase

01. Payment Flow

Flowchart

Polling


Webhook


1. Triggering createPayment API from Unifi Apps

When a user requests a purchase, the Unifi App Client sends the item information to the Unifi App Server.
The Unifi App Server then calls the createPaymentAPI on the Unifi Payment Server.
The createPayment API responds with a payment ID in the format: { id: <payment_id> }.

Request where pgType: CRYPTO | STRIPE

curl --location 'https://payment.dappportal.io/api/payment-v1/payment/create' \
--header 'X-Client-Id: {your_client_id}' \
--header 'X-Client-Secret: {your_client_secret}' \
--header 'Content-Type: application/json' \
--data '{
"buyerDappPortalAddress": "{user_wallet_address}", // user_wallet_address should be achieved through walletProvider.
"pgType": "{pg_type}",
"currencyCode": "{currency_code}",
"price": "{price}",
"paymentStatusChangeCallbackUrl": "{url_to_get_confirm_callback}",
"lockUrl": "{url_to_get_item_lock_callback}",
"unlockUrl": "{url_to_get_item_unlock_callback}",
"items": [
{
"itemIdentifier": "{your_item_identifier}",
"name": "{your_item_name}",
"imageUrl": "{your_item_image_url}",
"price": "{price}",
"currencyCode": "{currencyCode}"
}
],
"testMode": {true | false}
}'

2. Get paymentProvider from sdk instance.

The Unifi App Client retrieves the PaymentProvider instance from sdk with getPaymentProvider method.

const paymentProvider = sdk.getPaymentProvider()

3. Start payment process via paymentProvider.

Then, call the startPayment method of the paymentProvider, passing the paymentId received from step 1 as a parameter.

await paymentProvider.startPayment(paymentId)

4. Check payment status and finalize payment.

There are two ways to check the payment status:

Client-side: Wait for the promise returned by the startPayment method to resolve. When the promise resolves, the payment status is updated to CONFIRMED

Server-side
: Handle the payment status via a webhook event on the server. When using webhooks, the Unifi Payment Server sends an update to the paymentStatusChangeCallbackUrl specified in the parameters of the createPayment API whenever the payment status changes. The update request is sent as an HTTP POST , and the body includes the updated payment status in the following format:

{
  "paymentId": "{payment_id}",
  "status": "CONFIRMED"
}

If the received status is CONFIRMED, the Unifi App Server should send a request to finalize the payment by calling the following API:

curl --location --request 
POST 'https://payment.dappportal.io/api/payment-v1/payment/finalize' \
--header 'Content-Type: application/json' \
--data '{
    "id": "{payment_id}"
}'

5. In case of payment canceled by system.

If an HTTP 200 OK status is not returned in response to the webhook event, the event will be retried up to four times at exponential intervals: 1, 2, 4, and 8 seconds.

After all retry attempts have failed, if a lockUrl was specified when the payment was created, the Unifi Payment Server will send an unlock request as an HTTP POST . The body includes the paymentId and itemIdentifiers.

{
    "paymentId": "{payment_id}",
    "itemIdentifiers": ["{your_item_identifier}"]
}

This ensures that any reserved resources (e.g., NFT items, inventory) are released properly in case the payment status could not be confirmed via webhook.

6. Open payment history page

You can open payment history page via paymentProvider. Promise will be completed if payment history page opens successfully.

await paymentProvider.openPaymentHistory()

02. Payment API

baseUrl for Payment API: https://payment.dappportal.io

1. create payment

post /api/payment-v1/payment/create

Parameters

NameTypeDescription
X-Client-Id
string(header), requiredclient id obtained from support team
X-Client-Secret
string(header), requiredclient secret obtained from support team

Request Body

  • Example Value
{
"buyerDappPortalAddress": "{user_wallet_address}",
"pgType": "{pg_type}",
"currencyCode": "{currency_code}",
"price": "{price}",
"paymentStatusChangeCallbackUrl": "{url_to_get_status_change_callback_using_webhook}",
"lockUrl": "{url_to_get_item_lock_callback}",
"unlockUrl": "{url_to_get_item_unlock_callback}",
"items": [
{
"itemIdentifier": "{your_item_identifier}",
"name": "{your_item_name}",
"imageUrl": "{your_item_image_url}",
"price": "{price}",
"currencyCode": "{currencyCode}"
}
],
"testMode": {true | false}
}
  • Schema
{
buyerDappPortalAddress*: String,
pgType*: String(Enum: [STRIPE,CRYPTO]),
currencyCode: String(Enum: [USD,KRW,JPY,TWD,THB,KAIA,USDT]),
price: String,
paymentStatusChangeCallbackUrl*: String,
lockUrl: String,
unlockUrl: String,
items*: [Item {
itemIdentifier: String,
name: String,
imageUrl: String,
price: String,
currencyCode: String(Enum: [USD,KRW,JPY,TWD,THB,KAIA,USDT]),
}],
testMode*: Boolean,
}
fieldlimitation
buyerDappPortalAddressMax length : 42
pgType
  • STRIPE
  • CRYPTO
currencyCode
  • USD
  • KRW
  • JPY
  • TWD
  • THB
  • KAIA
  • USDT

It should be equal to Item's currencyCode
price
  • STRIPE
    • Put minimum unit following here
      • 100(cent) should be put if price is $1
    • Case1. Minimum unit equals to price unit
      • 10,000 KRW = 10000
      • 10,000 JPY = 10000
    • Case2. Minimum unit no equals to price unit
      • 10,000 USD = 1000000 (10,000 * 100)
      • 10,000 THB = 1000000 (10,000 * 100)
      • 10,000 TWD = 1000000 (10,000 * 100)

  • CRYPTO
    • KAIA
      • Put price as KAIA unit
      • 1 KAIA = 1.0
      • Up to 4 demcimal places
    • USDT
      • 1 USDT = 1.0
      • Up to 2 decimal places

It should be equal to sum of each price of Items.
  • Decimal Policy
  • STRIPE : n
paymentStatusChangeCallbackUrl for more detailMax length : 512

You can receive webhook whenever payment status was changed if you set paymentStatusChangerCallbackUrl.

Please use port 443 for entire connection. Webhooks cannot be received if a port other than 443 is used.
lockUrl for more detailMax length : 512
lockUrl for more detailMax length : 512
items(*)Only the purchase of single item is supported under current version
testModeThe payment methods according to testMode are as follows.
  • testMode : false
    • STRIPE : real mode
    • CRYPTO : kaia
  • testMode : true
    • STRIPE : test mode
    • CRYPTO : kairos

Items(*)

fieldlimitation
itemIdentifierMax length : 256
nameMax length : 256
imageUrlMax length : 512
pricePut minimum unit following here
For example, put 100(cent) if price is 1 Dollar.
currencyCode
  • USD
  • KRW
  • JPY
  • TWD
  • THB
  • KAIA
  • USDT

Responses

HTTP Status Codedescription
200Example Value
{
    "payment_id": "{payment_id}"
}

Schema
{
    payment_id*: String
}
400Example Value
{
    "code": 1001,
    "detail": "Invalid argument",
    "cause": null
}

Schema
{    
    code*: number,
    detail*: String,
    cause: String
}
401Example Value
{
    "code": 1007,
    "detail": "Invalid X-Client-Id or X-Client-Secret",
    "cause": null
}

Schema
{    
    code*: number,
    detail*: String,
    cause: String
}
403Example Value
{
    "code": 1007,
    "detail": "Access denied due to country restrictions.",
    "cause": null
}

Schema
{    
    code*: number,
    detail*: String,
    cause: String
}
500Example Value
{
    "code": 500,
    "detail": "Internal server error",
    "cause": null
}

Schema
{    
    code*: number,
    detail*: String,
    cause: String
}

Request Example


curl --location 'https://payment.dappportal.io/api/payment-v1/payment/create' \
--header 'X-Client-Id: {your_client_id}' \
--header 'X-Client-Secret: {your_client_secret}' \
--header 'Content-Type: application/json' \
--data '{
"buyerDappPortalAddress": "{user_wallet_address}",
"pgType": "{pg_type}",
"currencyCode": "{currency_code}",
"price": "{price}",
"paymentStatusChangeCallbackUrl": "{url_to_get_confirm_callback}",
"lockUrl": "{url_to_get_item_lock_callback}",
"unlockUrl": "{url_to_get_item_unlock_callback}",
"items": [
{
"itemIdentifier": "{your_item_identifier}",
"name": "{your_item_name}",
"imageUrl": "{your_item_image_url}",
"price": "{price}",
"currencyCode": "{currencyCode}"
}
],
"testMode": {true | false}
}'

2. get payment information

get /api/payment-v1/payment/info

Parameters

NameTypeDescription
X-Client-Id *required
string(header)client id obtained from support team
X-Client-Secret *required
string(header)client secret obtained from support team
id *required
string(query)payment id

responses

HTTP Status CodeDescription
200Example Value
{
    "id":"{payment id}",
    "buyerDappPortalAddress": "{user_wallet_address}",
    "pgType": "{pg_type}",
    "status": "{status}",
    "currencyCode": "{currency_code}",
    "price": "{price}",
    "decimal": {18}, //18 if cryptoCurrency is Kaia, 6 if cryptoCurrency is USDT
    "cryptoGasFee": "{blockchain_gas_fee}",
    "capturedAt": {1775207845888},
    "txHash": "{txHash_of_CRYPTO}",
    "usdExchangeRate": {usdExchangeRate}, //Fx rate at the completion of payment. It is only returned where pgType is STRIPE and status is CONFIRMED or FINALIZED.
    "usdExchangePrice": {usdExchangePrice}, //USD Price applied fx rate at the completion of payment. It is only returned where pgType is STRIPE and status is CONFIRMED or FINALIZED.
    "items": [
        {
            "itemIdentifier": "{your_item_identifier}",
            "name": "{your_item_name}",
            "imageUrl": "{your_item_image_url}",
            "price": "{price}",
            "currencyCode": "{currencyCode}"
        }
    ],
    "testMode": {true | false},
    "refund": {
      type: "REFUND",
      amount: "1000.0000000000000000000000000000", //Price policy follows . 
    }
}

Schema
{
    id*: String,     
    buyerDappPortalAddress*: String(maxLength: 42),
    pgType*: String(Enum: [STRIPE,CRYPTO]),
    status*: String(Enum: [CREATED,STARTED,REGISTERED_ON_PG,CAPTURED,CONFIRMED,CONFIRM_FAILED,FINALIZED,CANCELED])
    currencyCode*: String(Enum: [USD,KRW,JPY,TWD,THB,KAIA,USDT]) | null,
    price*: String | null,
    decimal: Integer,
    cryptoGasFee: String,
    capturedAt: Long,
    txHash: String,
    usdExchangeRate: String,
    usdExchangePrice: String,
    items*: [Item {
           itemIdentifier: String(maxLength: 256),
           name: String(maxLength: 256),
           imageUrl: String(maxLength: 512),
           price: String,
           currencyCode: String(Enum: [USD,KRW,JPY,TWD,THB,KAIA,UDST]),
        }]    
    testMode*: Boolean,
    refund: {
           type*: String(Enum:[CHARGEBACK, REFUND]),
           amount*: String,
           chargebackStatus: String(Enum:[NEEDS_RESPONSE, UNDER_REVIEW, WON, LOSE]), 
    }
}
404Example Value
{
    "code": 1002,
    "detail": "Not found payment",
    "cause": null
}

Schema
{
    code*: number,
    detail*: String,
    cause: String
}
401Example Value
{
    "code": 1007,
    "detail": "Invalid X-Client-Id or X-Client-Secret Header is not included when payment status is CONFIRMED or FINALIZED.",
    "cause": null
}

Schema
{
    code*: number,
    detail*: String,
    cause: String
}
403Example Value
{
    "code": 4030,
    "detail": "Access denied due to country restrictions.",
    "cause": null
}

Schema
{
    code*: number,
    detail*: String,
    cause: String
}
500Example Value
{
    "code": 500,
    "detail": "Internal server error",
    "cause": null
}

Schema
{
    code*: number,
    detail*: String,
    cause: String
}
fieldlimitation
id
buyerDappPortalAddressMax length : 42
pgType
  • STRIPE
  • CRYPTO
statusPayment status

  • CREATED
  • STARTED
  • REGISTERED_ON_PG
  • CAPTURED
  • CONFIRMED
  • CONFIRM_FAILED
  • FINALIZED
  • CANCELED
  • REFUNDED
  • CHARGEBACK
currencyCode
  • USD
  • KRW
  • JPY
  • TWD
  • THB
  • KAIA
  • USDT

It should be equal to Item's currencyCode
pricePut minimum unit as here.
For example, put 100(cent) if price is 1 Dollar. It should be equal to sum of each price of Items.

  • Decimal Policy
    • STRIPE : no
    • CRYPTO : Up to 4 decimal places
decimalThis value should be interpreted together with price. It represents the number of decimal places for the given currency. For example, it is 18 for KAIA, 6 for USDT, and 0 for KRW (Stripe payments).
cryptoGasFeeThe gas fee of the blockchain used for crypto payments.
capturedAtThe timestamp when the payment was approved, in milliseconds since the Unix epoch.

  • stripe: The time when the payment was authorized by Stripe
  • crypto: The block creation time that includes the transaction
txHashThe transaction hash of the payment (applicable only for crypto payments).
usdExchangeRateFx rate at the completion of payment. It is only returned where pgType is STRIPE and status is CONFIRMED or FINALIZED.
usdExchangePriceUSD Price applied fx rate at the completion of payment. It is only returned where pgType is STRIPE and status is CONFIRMED or FINALIZED.
items(*)Only the purchase of single item is supported under current version
testModeThe payment methods according to testMode are as follows.

  • testMode : false
    • stripe : real
    • mode crypto : kaia

  • testMode : true
    • stripe : testmode
    • crypto : kairos
refund
refund.type
  • CHARGEBACK
  • REFUND
refund.amountPrice policy follows here.
refund.chargebackStatus
  • NEEDS_RESPONSE: A CHARGEBACK has occurred, but it is in the state before contesting or challenging it.
  • UNDER_REVIEW: The dispute has been filed and is currently under review by STRIPE.
  • WON: If the dispute is accepted.
  • LOST: If the dispute result is not accepted

Items(*)

fieldlimitation
itemIdentifierMax length : 256
nameMax length : 256
imageUrlMax length : 512
priceFor example, put 100(cent) if price is 1 Dollar.
currencyCode
  • USD
  • KRW
  • JPY
  • TWD
  • THB
  • KAIA
  • USDT

Request Example

curl --location 'https://payment.dappportal.io/api/payment-v1/payment/info?id={payment_id}' \
--header 'X-Client-Id: {your_client_id}' \
--header 'X-Client-Secret: {your_client_secret}'

3. get payment status

get /api/payment-v1/payment/status

Parameters

NameTypeDescription
id
string(header), requiredpayment id

Responses

HTTP Status CodeDescription
200Example Value
{
    "status": "{status}"
}

Schema
{
    status*: String (Enum:[CREATED, STARTED, REGISTERED_ON_PG, CAPTURED, CONFIRM_FAILED,FINALIZED,CANCELED]
}
403Example Value
{
    "code": 4030,
    "detail": "Access denied due to country restrictions.",
    "cause": null
}

Schema
{
    code*: number,
    detail*: String,
    cause: String
}
404Example Value
{
    "code": 1002,
    "detail": "Not found payment",
    "cause": null
}

Schema
{
    code*: number,
    detail*: String,
    cause: String
}
500Example Value
{
    "code": 500,
    "detail": "Internal server error",
    "cause": null
}

Schema
{
    code*: number,
    detail*: String,
    cause: String
}
fieldlimitation
statusPayment status

  • CREATED
  • STARTED
  • REGISTERED_ON_PG
  • CAPTURED
  • CONFIRMED
  • CONFIRM_FAILED
  • FINALIZED
  • CANCELED
  • REFUNDED
  • CHARGEBACK

Request Example

curl --location 'https://payment.dappportal.io/api/payment-v1/payment/status?id={payment_id}'

4. finalize payment

It will be ready to be settled after requesting finalize payment API.

You can't get settlement for STRIPE transaction if you did not request finalize payment API.



For CRYPTO, We recommend to request finalize payment API too.

Parameters

  • N/A

Request Body

Example ValueSchema
{
    id: "{payment_id}" 
}
{
    id*: String
}

Responses

HTTP Status CodeDescription
200N/A
403Example Value
{
    "code": 1004,
    "detail": "Invalid payment status.",//Current payment status cannot complete payment finalization
    "cause": null
}

Schema
{    
    code*: number,
    detail*: String,
    cause: String
}
403Example Value
{
    "code": 4030,
    "detail": "Access denied due to country restrictions.",
    "cause": null
}

Schema
{    
    code*: number,
    detail*: String,
    cause: String
}
404Example Value
{
    "code": 1002,
    "detail": "Not found payment",
    "cause": null
}

Schema
{    
    code*: number,
    detail*: String,
    cause: String
}
500Example Value
{
    "code": 500,
    "detail": "Internal server error",
    "cause": null
}

Schema
{    
    code*: number,
    detail*: String,
    cause: String
}

Request Example

curl --location 'https://payment.dappportal.io/api/payment-v1/payment/finalize' \
--header 'Content-Type: application/json' \
--data '{
"id": "{payment_id}"
}'

03. PaymentProvider

sdk.getPaymentProvider()

Initializes the paymentProvider, allowing developers to use various payment features.

const paymentProvider = sdk.getPaymentProvider();

Parameters

  • N/A

Responses

PaymentProvider

paymentProvider.startPayment()

Calling paymentProvider.startPayment() displays a transaction window to the user and begins the payment process.

Parameters

  • payment_id *required · string

Responses

Promise<unknown>

Error

CodeDescription
-31001This error is triggered when a STRIPE payment gets canceled.
{
code: -31001,
message:'Payment is canceled by user or timeout'
}
-31002
{
code: -31002,
message:'Payment is failed'
}
-32001This error is triggered when a CRYPTO payment gets canceled.
{
code: -32001,message:'User denied transaction send.'
}

paymentProvider.openPaymentHistory()

This method opens a payment history window. In order to complete the operation, a signature from the user is required.

Parameters

  • N/A

Responses

Promise<void>

04. Payment Webhook

Each Webhook's callback URL should be set differently.

If lockUrl and unlockUrl is not needed, please enter null.

1. lock event

lockUrl is a server endpoint that is called to temporarily lock or reserve the item being purchased. This is typically required for items with limited quantity or NFTs, to prevent overselling and ensure that no other user can purchase the item while the current payment is in progress.

If the lockUrl parameter is included in the create payment request, a POST method request for the lock webhook event will be made.

When the payment is initiated using the SDK’s startPayment function, the webhook event is requested just before starting the user’s payment flow if it is determined that the payment can proceed normally.

If a request is made with the lockUrl but a 200 response is not received, the payment will be immediately canceled and marked as failed automatically.

{
"paymentId": "{payment_id}",
"itemIdentifiers": [
{
"{your_item_identifier}"
}
]
}

2. unlock event

unlockUrl is a server endpoint that is called when a payment fails, is canceled, or times out, in order to release the previously locked item and make it available for others to purchase again.

If the unlockUrl parameter is included in the create payment request, a POST request will be sent to this endpoint when a payment failure-related event is triggered.

If the payment is successfully completed (status === CONFIRMED), the unlockUrl will not be called. Instead, the item is typically finalized via a separate process (finalize API or internal logic).

If a request is made with the unlockUrl but a 200 response is not received, it will be retried up to 5 times at intervals of 1, 2, 4 and 8 seconds. To avoid that the item remains locked, implementing background job to attempt unlocking again is recommended.

{
"paymentId": "{payment_id}",
"itemIdentifiers": [
{
"{your_item_identifier}"
}
]
}

3. payment status change event

When creating a payment request, a payment status change webhook event will be sent to the paymentStatusChangeCallbackUrl you've provided.

An event occurs whenever the payment status changes, except when the status is CREATED.

If the status of the received webhook is CONFIRMED, you can call the finalize payment API.

If a request to the paymentStatusChangeCallbackUrl does not receive a 200 response, a maximum of 5 retries will be made at intervals of 1, 2, 4, and 8 seconds. Even if a 200 response is not received after 5 retries, the payment cancellation will not proceed.

{
"paymentId": "{payment_id}",
"status": "STARTED|REGISTERED_ON_PG|CAPTURED|CONFIRMED|CONFIRM_FAILED|FINALIZED|CANCELED|REFUNDED|CHARGEBACK",
"cryptoPaymentInfo": { // added when "status" is CONFIRMED
"paymentTxHash": "0x.."
}
}
StatusDescription
CREATEDHosted create payment API but not host startPayment of SDK
STARTEDHosted startPayment but await payment approval from user (STRIPE/CRYPTO)
REGISTERED_ON_PGOnly for pgType = CRYPTO) Transaction has been approved but await for enough block confirmation to prevent chain re-organization. - at least after 10 block confirmation from user's request for KAIA
CAPTURED(Only for pgType = CRYPTO) Checking validity of the transaction after 10 blocks have confirmed from the user's transaction request
CONFIRMEDPayment approval is completed and await payment finalization from Unifi App
CONFIRM_FAILED(Only for pgType = CRYPTO) Failed to check validity of the transaction after 10 blocks have confirmed from user's transaction request
FINALIZEDPayment process is done with payment approval and payment finalization from Unifi App - In case of pgType is CRYPTO, transactions will be automatically finalized after 5 minutes once it reaches the CONFIRMED status if the finalize payment API is not hosted.
CANCELEDPayment is cancelled as Policy for payment cancellation
REFUNDEDPayment has been refunded
CHARGEBACK
Users have claimed chargeback directly.

This Webhook message will be sent at each stage of the process, including the occurrence of a CHARGEBACK.

(*) This status can only be queried through the get payment information API.