Common

API Endpoints #

Common #

GET v1/token #

https://tw.supplier.yahoo.com/api/spa/v1/token

取得 X-YahooWSSID-Authorization HTTP Request Header,用做後續其他 Endpoint 認證使用。
此 Token 為根據 Cookie 所計算,使用者端可以 Cookie Value 暫存此 Token 的值,不需每個 Request 都重覆計算。

Output

  • HTTP Status Code: 200 (OK)
  • Object: Token
Error
HTTP Status Code Error Code Description
400 Invalid input
400 40000005 Contains illegal character(s)
401 40100001 Missing or bad authentication
401 40100002 Invalid cookie
401 40100003 Missing or bad authentication
401 40100004 Check wssid failed
401 40100005 Issue wssid failed
401 40100006 Cookie has expired
500 50000006 Internal server error

Example

  • Get token by cookie
GET /api/spa/v1/token
Content-Type: application/json; charset=utf-8
Cookie: _sp=<the cookie fetched by following SignIn section>

HTTP/1.1 200 OK

{
  "wssid": "ABCDEFabcdef0123456789._-~"
}

POST v1/signIn #

https://tw.supplier.yahoo.com/api/spa/v1/signIn

使用 SCM API Key 取得 Cookie。

Payload: String

Output

  • HTTP Status: 204 (No Content)
Error
HTTP Status Code Error Code Description
400 40000005 Contains illegal character(s)
401 40100001 Missing or bad authentication
401 40100002 Invalid cookie
401 40100003 Missing or bad authentication
401 40100004 Check wssid failed
401 40100006 Cookie has expired
500 50000006 Internal server error

Example

  • Get token by cookie
POST /api/spa/v1/signIn
api-token: <token>
api-keyversion: <version>
api-timestamp: <timestamp>
api-signature: <signature>
<ciphertext>
欄位說明

Sign In 至本 API 的 HTTP request 需包含 4 個 header 與 Credential 密文,分別說明如下

Data
Location 說明
api-token Header 即為 API Key 查詢頁提供之 Token
api-keyversion Header 即為 API Key 查詢頁提供之 Version
api-timestamp Header 為 Request 發送當下的 Unix Timestamp,例如 1548225833,此 Timestamp 的有效期限為 90 秒
api-signature Header Cookie 密文的簽章,由 Timestamp, Token, SaltKey 與密文計算而成,可參考下方範例程式
若計算出的簽章為大寫英文時,需將簽章轉換為小寫英文。例如 AB12CD 需轉換為 ab12cd
Credential Body 加密後的 Cookie,可參考下方範例程式
HTTP/1.1 204 No Content
Set-Cookie: <cookie>

Sign in 完成後會由 Response Set-Cookie Header 回傳 _sp Cookie,其效期為 6 小時。存其本 API 其他各 Endpoint 都需於 Header 加上此 Cookie 欄位。

Java 範例程式
import com.github.longhorn.fastball.time.Clock;
import com.yahoo.parsec.clients.ParsecAsyncHttpRequest;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;
import org.json.JSONObject;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.ws.rs.core.NewCookie;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;

public class Sample {
    public ParsecAsyncHttpRequest signIn() throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, UnsupportedEncodingException, BadPaddingException, IllegalBlockSizeException {
        // Required data
        String timestamp = Clock.fromNow().getEpochSecondString();
        String token = "Supplier_1234";
        int supplierId = 1234;
        String secretKey = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef01234567890=";
        String iv = "ABCDEFabcdef01234567890=";
        String saltKey = "ABCDEFGHIJKabcdefghij01234567890";
        String version = "1";

        // Credential Plaintext
        JSONObject cookie = new JSONObject();
        cookie.put("supplierId", supplierId);
        String plaintext = cookie.toString();

        // Credential Ciphertext
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        SecretKeySpec secretKeySpec = new SecretKeySpec(Base64.decodeBase64(secretKey), "AES");
        IvParameterSpec ivParameterSpec = new IvParameterSpec(Base64.decodeBase64(iv));
        cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
        String ciphertext = Base64.encodeBase64String(cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8.name())));

        // Credential Signature
        String signatureSource = String.format("%s%s%s%s", timestamp, token, saltKey, ciphertext);
        SecretKey signatureSecretKey = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8.name()), "HmacSHA512");
        Mac mac = Mac.getInstance("HmacSHA512");
        mac.init(signatureSecretKey);
        byte[] rawHmac = mac.doFinal(signatureSource.getBytes(StandardCharsets.UTF_8.name()));
        byte[] hexBytes = new Hex().encode(rawHmac);
        String signature = new String(hexBytes, StandardCharsets.ISO_8859_1.name());

        return new ParsecAsyncHttpRequest.Builder()
                .setUrl("https://tw.supplier.yahoo.com:443/api/spa/v1/signIn")
                .setMethod(HttpMethod.POST)
                .setHeader("api-token", token)
                .setHeader("api-keyversion", version)
                .setHeader("api-timestamp", timestamp)
                .setHeader("api-signature", signature)
                .setBody(ciphertext)
                .build();
    }
}
PHP 範例程式
<?php
class AES
{//{{{
    const OPENSSL_CIPHER_NAME = "aes-256-cbc";
    const CIPHER_KEY_LEN = 32;

    private $_key;
    private $_iv;

    public function __construct($key, $iv)
    {
        $base64decodedKey = base64_decode($key);
        if (strlen($base64decodedKey) < AES::CIPHER_KEY_LEN) {
            $this->_key = str_pad($base64decodedKey, AES::CIPHER_KEY_LEN, "0");
        } else if (strlen($base64decodedKey) > AES::CIPHER_KEY_LEN) {
            $this->_key = substr($base64decodedKey, 0, AES::CIPHER_KEY_LEN);
        } else {
            $this->_key = $base64decodedKey;
        }
        $this->_iv = base64_decode($iv);
    }

    public function encrypt($data)
    {
        return base64_encode(openssl_encrypt($data, AES::OPENSSL_CIPHER_NAME, $this->_key, OPENSSL_RAW_DATA, $this->_iv));
    }
}//}}}

class HMac
{//{{{
    private $_secretKey;

    public function __construct($secretKey)
    {
        $this->_secretKey = $secretKey;
    }

    public function sha512($data)
    {
        return hash_hmac("sha512", $data, $this->_secretKey);
    }
}//}}}

class Sample
{
    private $shareSecretKey = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef01234567890=';
    private $shareSecretIv = 'ABCDEFabcdef01234567890=';
    private $saltKey = 'ABCDEFGHIJKabcdefghij01234567890';
    private $apiVersion = '1';
    private $token = 'Supplier_1234';

    private function getCookie($serviceUrl, $messageBody)
    {
        $timestamp = time();

        $aes = new AES($this->shareSecretKey, $this->shareSecretIv);
        $cipherText = $aes->encrypt($messageBody);

        $hasher = new HMac($this->shareSecretKey);
        $signature = $hasher->sha512(sprintf("%s%s%s%s", $timestamp, $this->token, $this->saltKey, $cipherText));

        $headers = [
            "Content-Type: application/json; charset=utf-8",
            "api-token: $this->token",
            "api-signature: $signature",
            "api-timestamp: $timestamp",
            "api-keyversion: $this->apiVersion",
        ];

        $httpRequest = curl_init();
        curl_setopt($httpRequest, CURLOPT_URL, $serviceUrl);
        curl_setopt($httpRequest, CURLOPT_POSTFIELDS, $cipherText);
        curl_setopt($httpRequest, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($httpRequest, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($httpRequest, CURLOPT_POST, true);
        curl_setopt($httpRequest, CURLOPT_HEADER, true);
        $response = curl_exec($httpRequest);
        $responseHeader = curl_getinfo($httpRequest);
        curl_close($httpRequest);

        if ($responseHeader['http_code'] == 204) {
            preg_match_all('/^Set-Cookie:\s(?\'cookie\'_sp=.+)$/mi', $response, $matches);
            return trim($matches['cookie'][0]);
        } else {
            throw new Exception("Failed to obtain the SCM cookie");
        }

    }

    public function main()
    {
        $serviceUrl = "https://tw.supplier.yahoo.com:443/api/spa/v1/signIn";
        $requestContent = '{"supplierId":"1234"}';
        $cookie = $this->getCookie($serviceUrl, $requestContent);
        echo $cookie."\n"; // contains the SCM cookie key and value
    }
}

$sample = new Sample;
$sample->main();
Node JS 範例程式
const crypto = require('crypto')
// npm i node-fetch
const fetch = require('node-fetch')

// Required data
const timestamp = Math.round(Date.now() / 1000).toString()
const supplierId = 1234
const token = `Supplier_1234`
const secretKey = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef01234567890='
const iv = 'ABCDEFabcdef01234567890='
const saltKey = 'ABCDEFGHIJKabcdefghij01234567890'
const version = '1'

const getSetCookie = async () => {
  // Credential Plaintext
  const plaintext = JSON.stringify({ supplierId })

  // Credential Ciphertext
  const cipher = crypto.createCipheriv(
    'aes-256-cbc',
    Buffer.from(secretKey, 'base64'),
    Buffer.from(iv, 'base64')
  )
  const cipherText = `${cipher.update(plaintext, 'utf8', 'base64')}${cipher.final('base64')}`

  // Credential Signature
  const hmac = crypto.createHmac('sha512', secretKey)
  const signatureSource = `${timestamp}${token}${saltKey}${cipherText}`
  const signature = hmac.update(signatureSource).digest('hex')

  const url = 'https://tw.supplier.yahoo.com/api/spa/v1/signIn'
  const headers = {
    Accept: 'application/json',
    'Content-Type': 'application/json',
    'api-token': token,
    'api-signature': signature,
    'api-timestamp': timestamp,
    'api-keyversion': version,
    'api-supplierid': supplierId
  }

  const resp = await fetch(url, { method: 'POST', headers, body: cipherText })
    .then(res => ({
      status: res.status,
      statusText: res.statusText,
      setCookie: res.headers.get('set-cookie')
    }))
    .catch(e => e)

  console.log('response: ', resp)
}

getSetCookie()
C# 範例程式
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using Newtonsoft.Json;

internal class Program
{
    private static void Main(string[] args)
    {
        // Required data
        var timestamp = GetTimestamp().ToString();
        var supplierId = 1234;
        var apiTtoken = "Supplier_1234";
        var apiKeyValue = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef01234567890=";
        var apiKeyIV = "ABCDEFabcdef01234567890=";
        var saltKey = "ABCDEFGHIJKabcdefghij01234567890";
        var apiVersion = "1";

        // Credential Plaintext
        var text = new { supplierId = supplierId };
        var plaintext = JsonConvert.SerializeObject(text);

        // Credential Ciphertext
        var aes = Aes.Create();
        aes.Key = Convert.FromBase64String(apiKeyValue);
        aes.IV = Convert.FromBase64String(apiKeyIV);
        aes.Mode = CipherMode.CBC;
        aes.Padding = PaddingMode.PKCS7;
        var cipher = aes.CreateEncryptor(aes.Key, aes.IV);
        var ciphertextBytes = cipher.TransformFinalBlock(Encoding.UTF8.GetBytes(plaintext), 0, plaintext.Length);
        var ciphertext = Convert.ToBase64String(ciphertextBytes);
        aes.Clear();

        // Credential Signature
        var encoder = new UTF8Encoding();
        var keyBytes = encoder.GetBytes(apiKeyValue);
        var hMAC = new HMACSHA512(keyBytes);
        var hash = hMAC.ComputeHash(encoder.GetBytes($"{timestamp}{apiTtoken}{saltKey}{ciphertext}"));
        var signature = BitConverter.ToString(hash).Replace("-", string.Empty).ToLower();

        using (var client = new HttpClient())
        {
            client.DefaultRequestHeaders.Accept.Clear();
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            client.DefaultRequestHeaders.Add("api-token", apiTtoken);
            client.DefaultRequestHeaders.Add("api-signature", signature);
            client.DefaultRequestHeaders.Add("api-timestamp", timestamp);
            client.DefaultRequestHeaders.Add("api-keyversion", apiVersion);
            client.Timeout = new TimeSpan(0, 2, 0);
            string apiUrl = "https://tw.supplier.yahoo.com/api/spa/v1/signIn";
            var content = new StringContent(ciphertext, Encoding.UTF8, "application/json");
            var response = client.PostAsync(apiUrl, content).Result;
        }
    }

    private static int GetTimestamp()
    {
        var unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
        return (int)(DateTime.Now.ToUniversalTime() - unixEpoch).TotalSeconds;
    }
}