# Quick Start

## Endpoint

{% hint style="info" %}
<https://sandbox-api-merchant.wasabicard.com>
{% endhint %}

You need to contact the open platform staff to add the merchant server's IP whitelist for prod env.

## Glossary

1. `api-key`: Unique identifier of the merchant.&#x20;
2. `public-key`: User's RSA public key.&#x20;
3. `private-key`: User's RSA private key.&#x20;
4. `wsb-public-key`:  Platform RSA public key.

The above parameters can be obtained and updated in the dashboard.&#x20;

## Authentication&#x20;

Use api-key to authenticate the request. User need to put `X-WSB-API-KEY` in the request header every time they make a request.

## Signature

To ensure the security of API calls, each request and response needs to be verified for message integrity through signature.&#x20;

User requests API are signed with user's RSA private key and verified with merchant RSA public key;&#x20;

User requests responses are signed with platform RSA private key and verified with platform RSA public key;&#x20;

Notification subscriptions are signed with platform RSA private key and verified with platform RSA public key.

The signing rules are as follows:

1. The merchant sends a request to the development platform, signs it with the `user's RSA private key`, and the open platform verifies the signature with the `user's RSA public key`;
2. The open platform interface responds and sends notifications to the merchant, signs it with the `platform's RSA private key`, and the merchant verifies the signature with the `platform's RSA public key`;
3. Signing is to encrypt the `HTTP request body`, and all requests are called using the POST method;
4. Use `sha256 RSA` to sign it and then use `base64` as the signature string;
5. Put the encrypted signature string signature in the HTTP header `X-WSB-SIGNATURE` field.

{% hint style="info" %}
If the interface does not require request parameters, please construct an empty JSON object for body for transmission and signing.
{% endhint %}

## Code

{% tabs %}
{% tab title="ResultData.class" %}

```java
package com.wsbcard.demo.model;

import lombok.Data;
import java.io.Serializable;

/**
 * @author: test
 * @date: 2023/12/2 15:20
 * @describe:
 */
@Data
public class ResultData<T> implements Serializable {

    private static final long serialVersionUID = 1L;

    private boolean success = true;

    private int code = 200;

    private String msg = "Success";

    private T data;


    public boolean isSuccess() {
        return code == 200;
    }

}
```

{% endtab %}

{% tab title="TwoTuple.class" %}

```java
package com.wsbcard.demo.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/**
 * @Author: test
 * @date: 2024/8/4 14:28
 * @describe:
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TwoTuple<R, S> implements Serializable {

    private R first;
    private S second;



}
```

{% endtab %}

{% tab title="ToolUtil.class" %}

```java
package com.wsbcard.demo;

import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.wsbcard.demo.model.ResultData;
import com.wsbcard.demo.model.TwoTuple;
import lombok.SneakyThrows;
import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;


/**
 * @Author: test
 * @date: 2025/9/27 13:47
 * @describe:
 */
public class ToolUtil {

    private static final Log logger = LogFactory.get();

    private static final int MAX_DECRYPT_BLOCK = 128;
    public static final String KEY_ALGORITHM = "RSA";
    public static final String SIGNATURE_ALGORITHM_SHA256 = "SHA256withRSA";

    public static final String HEADER_SIGNATURE = "X-WSB-SIGNATURE";
    public static final String HEADER_API_KEY = "X-WSB-API-KEY";

    public static final String ENDPOINT = "https://sandbox-api-merchant.wasabicard.com";

    public static final String API_KEY = "c0f8eb09-4407-4f49-b49a-b2143b76989e-5f730e10-6fa9-4ee6-aee0-9259f6479a42";
    public static final String PRIVATE_KEY = "MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAJqlHaK7sct6ukEQv1N97+5LK+heHdq6Uc9NPUKlud+QRubFeyxzDWXPVnhFwcZq2euzxer7lr2vElUZUA/Bd+Wc8Omf21zU/B8BGoZAmcCTNcOOKCoTYkqO5IC8nd602vf14Fjnnhi1BZrfWaQZyzywFkYPn21RC2lEi0NdPHe9AgMBAAECgYBDacEehIW/9xMkdAGDiv3BukE8vXJ0PA6XuMsAt6/sVj+iL+o++TSY8iZ2raoILeIrjqWzhtMygxQRxsQQ6jyyTRNNnSw8gySdO1WwG5EwqBchgEtb09Eq986l5/UZ0yjq2CAkG4lksBn3rZFmjcD3V1qM3TfaaNrMJbg3keZ//QJBAMrjqpyvI8H3ZyL3LwVxjETrR2/ZPKewO8GRSFnfuK40IUTWgUhmeRIlP3hxnzCdJGbsV8pEL7stbUbyUIaA4dcCQQDDIGzJ5ZsjMQRSR3m8l7N5dPQbrx6/g7lA/njRLyC1vQQ4hnm1A6rFIKS9sxY9Vbo7FBUb43cknDZbO/E3QeiLAkBFh03BkjeD0j/y+JiRmf8C40pA50ZlBP0Fcb9EpWmJsW2xRH1bjVpyQHeG1BFEvKVr7BSNyV4+G+w2AvZbkbFTAkBeWWphAXXPQV9OjaMOjufIXcW/MSEUB5RnGeS/eSM+3UOSNWvaHAjRwqQF2fdx9ubStDY0wvsUJ7icytIrHuJbAkAE0wkNMqHqorTmjcv2wGnQS1XZghWNuZMdEj/2Ffz9y6LQtwqXDT/PqpdO5vySHr6w+GxLDpNPHb1TmxoVvuPb";
    public static final String WSB_PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCyyPUFA1Zu2GFS2DdPL6xP5bGmPG9dFMSXpDy3GykW/lL4VR24UsjKs5h6g8OfSOuOze3yC4jWE/IBW5/L9xM9kGvFQK/S1zr44hMnDn+7S5MSi+jnZDRIrCRQJTqkwfpy39h2cQ4xlHKfbEjkkeo5ZQ0VFgRHYPr7b10l+3j8gwIDAQAB";


    /**
     * http post
     * @param url
     * @param body
     * @return
     */
    public static TwoTuple<String, String> post(String url, String body) {
        try {
            String signature = signSha256(body, PRIVATE_KEY);

            Map<String, String> headerMap = new HashMap<>(2);
            headerMap.put(HEADER_API_KEY, API_KEY);
            headerMap.put(HEADER_SIGNATURE, signature);

            HttpRequest httpRequest = HttpRequest.post(url).body(body);
            httpRequest.addHeaders(headerMap);

            HttpResponse response = httpRequest.execute();
            logger.info("sendPostJson url {}, request:{}, response:{}", url, body, response.body());

            return new TwoTuple<>(response.header(HEADER_SIGNATURE), response.body());
        } catch (Exception e) {
            logger.error("error", e);
        }
        return null;
    }


    /**
     * Sign by share256
     * @param data body
     * @param privateKey
     * @return
     * @throws Exception
     */
    public static String signSha256(String data, String privateKey) throws Exception {
        byte[] keyBytes = Base64.getDecoder().decode(privateKey);
        PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
        PrivateKey privateK = keyFactory.generatePrivate(pkcs8KeySpec);
        Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM_SHA256);
        signature.initSign(privateK);
        signature.update(data.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(signature.sign());
    }

    public static boolean verify(String data, String publicKey, String sign) throws Exception {
        byte[] keyBytes = Base64.getDecoder().decode(publicKey);
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
        PublicKey publicK = keyFactory.generatePublic(keySpec);
        Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM_SHA256);
        signature.initVerify(publicK);
        signature.update(data.getBytes(StandardCharsets.UTF_8));
        return signature.verify(Base64.getDecoder().decode(sign));
    }


    /**
     * verify signature
     *
     * @param result <Signature, Response body data></>
     */
    public static void verifySignature(TwoTuple<String, String> result) throws Exception {
        ResultData resultData = JSON.parseObject(result.getSecond(), ResultData.class);
        if (resultData.isSuccess()) {
            boolean verify = verify(result.getSecond(), WSB_PUBLIC_KEY, result.getFirst());
            logger.info("verify signature result:{}", verify);
        }
    }
    
    /**
     * Decrypt data using the private key
     *
     * @param encryptedData
     * @param privateKey
     * @return
     */
    @SneakyThrows
    public static String decryptPrivateKey(String encryptedData, String privateKey) {
        if (encryptedData.contains("%")) {
            encryptedData = URLDecoder.decode(encryptedData, "UTF-8");
        }
        byte[] rs = Base64.getDecoder().decode(encryptedData);
        return new String(decryptByPrivateKey(rs, privateKey), StandardCharsets.UTF_8);
    }


    /**
     * Decrypt data using the private key
     *
     * @param encryptedData
     * @param privateKey Private key (BASE64 encoded)
     */
    @SneakyThrows
    public static byte[] decryptByPrivateKey(byte[] encryptedData, String privateKey) {
        byte[] keyBytes = Base64.getDecoder().decode(privateKey);
        PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
        Key privateK = keyFactory.generatePrivate(pkcs8KeySpec);
        Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
        cipher.init(Cipher.DECRYPT_MODE, privateK);
        int inputLen = encryptedData.length;
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        int offSet = 0;
        byte[] cache;
        int i = 0;
        // Decrypt data in segments
        while (inputLen - offSet > 0) {
            if (inputLen - offSet > MAX_DECRYPT_BLOCK) {
                cache = cipher.doFinal(encryptedData, offSet, MAX_DECRYPT_BLOCK);
            } else {
                cache = cipher.doFinal(encryptedData, offSet, inputLen - offSet);
            }
            out.write(cache, 0, cache.length);
            i++;
            offSet = i * MAX_DECRYPT_BLOCK;
        }
        byte[] decryptedData = out.toByteArray();
        out.close();
        return decryptedData;
    }


    public static void main(String[] args) throws Exception {
        /**************** call interface ****************/
        String uri = "/merchant/core/mcb/card/v2/cardTypes";

        TwoTuple<String, String> result = post(ENDPOINT + uri, new JSONObject().toJSONString());
        if (result != null) {
            verifySignature(result);
        }

        /**************** decryptData ****************/

        String encryptedCvv = "h1J5ZnOiboxn/dkNg3hmIP0v1vfRUzkqRh3AULt9nTBNAtVwtTzWXkVN3alXqTGhavfRf11lfSyKDbKFg9G0woTdiyB/bRRQO0GECk3KWdbVIIpML1cwodb4kkcqOjfILg8qDLIJb+XJuKWpd8Ous4S8AQRtjjA2KeKqXNghvRM=";
        System.out.println("CVV:" + decryptPrivateKey(encryptedCvv, PRIVATE_KEY));

    }
}

```

{% endtab %}
{% endtabs %}

## Java Demo

Contains code information such as mobile area code, region, etc.; and java-demo

{% file src="<https://83277272-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FK4pYjaHDLD6kKvvd7mig%2Fuploads%2FD2bcy9Xa8YxawRTK6JkZ%2Fdemo.zip?alt=media&token=8bc6cd79-676d-4c17-9c67-9a4d2f62227b>" %}

## Request Template

<mark style="color:green;">`POST`</mark> `${uri}`

**Headers**

| Name            | Value              |
| --------------- | ------------------ |
| Content-Type    | `application/json` |
| X-WSB-API-KEY   | `${api-key}`       |
| X-WSB-SIGNATURE | `${signature}`     |

## Response Template

**Headers**

| Name            | Value              |
| --------------- | ------------------ |
| Content-Type    | `application/json` |
| X-WSB-SIGNATURE | `${signature}`     |

**Response**

```json
{
    "success": true, //True when code=200
    "code": 200,
    "msg": "Success",
    "data": null
}
```

## Response Code

<table><thead><tr><th width="121">code</th><th>Remark</th></tr></thead><tbody><tr><td>200</td><td>Success</td></tr><tr><td>500</td><td>Internal service error. Please contact WasabiCard staff</td></tr><tr><td>-1</td><td>Business error code. This is a normal situation, for example, the user purchases more cards than the limit.</td></tr><tr><td>40002</td><td>Param error</td></tr><tr><td>40020</td><td>Duplicate order number</td></tr><tr><td>40021</td><td>Insufficient balance</td></tr><tr><td>50001</td><td>The api key and signature parameters are empty</td></tr><tr><td>50002</td><td>Signature verification failed</td></tr><tr><td>50003</td><td>Signature failed</td></tr><tr><td>50004</td><td>User does not exist</td></tr></tbody></table>
