Skip to content

Quickstart (5 minutes) ​

This page does one thing: get you to your first payment and your first callback as fast as possible. Throughout, API_BASE is your integration host (for example https://api.web3sdk.com — the address you receive at onboarding is authoritative).

Step 0: Get an API KEY ​

Submit your merchant details through the website registration entry point. Once approved, we issue an API KEY for your merchant number.

Self-service registration is live

Your API KEY is not returned by the registration request. After signing up, open the merchant dashboard → “Payments → Payment settings” and click Reveal to see the current key (every reveal is audit-logged; we never email the key). To rotate it, click Reset on the same page; the old key remains valid during a transition window.

Keep the KEY in a server-side environment variable — not in your repository, not in the frontend:

bash
export PAY_API_KEY='<your API KEY>'
export API_BASE='https://api.web3sdk.com'

The merchant-side endpoint GET /api/pay/getMyProfile returns your merchantNo, collectName, a maskedapiKey and your cashierBaseUrl. The plaintext KEY is shown only twice: when issued, and when explicitly reset.

Step 1: Configure a collection address ​

Log in to the merchant console, add a collection address for an authorized chain and set it active. Merchant-side endpoints:

http
GET {API_BASE}/api/pay/getMyChains

Once you have the chain and token ids (the checkout page uses the same list):

bash
curl -X POST "$API_BASE/api/pay/saveMyAddress" \
  -H 'Content-Type: application/json' \
  -H 'x-token: <JWT from the merchant console login>' \
  -d '{
    "chainId": 2,
    "address": "TXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    "label": "Primary collection address"
  }'

Then make it the active one:

bash
curl -X POST "$API_BASE/api/pay/setActiveAddress" \
  -H 'Content-Type: application/json' \
  -H 'x-token: <JWT from the merchant console login>' \
  -d '{"chainId": 2, "address": "TXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"}'

The collection address decides where the money goes

When an order is created, the active address at that moment is snapshotted into the order. Changing your address later does not affect orders already created — buyers still send to the old address and we still watch it.

Step 2: Create an order from your server ​

Call the Open API from your own server. The header carries the API KEY, and amount is a decimal string in token units:

bash
curl -X POST "$API_BASE/api/pay/open/createOrder" \
  -H "X-Pay-Api-Key: $PAY_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "merchantOrderNo": "SHOP-2026-0001",
    "chainId": 2,
    "tokenId": 1,
    "amount": "100.5",
    "subject": "Order SHOP-2026-0001",
    "notifyUrl": "https://your-shop.com/pay/notify",
    "returnUrl": "https://your-shop.com/pay/done"
  }'

Response (abridged):

json
{
  "code": 0,
  "data": {
    "order": {
      "orderNo": "PO20261011XXXXXXXXXXXX",
      "merchantOrderNo": "SHOP-2026-0001",
      "merchantNo": "M00000XXXX",
      "status": 1,
      "statusText": "待支付",
      "orderType": 1,
      "amountMinimal": "100500000",
      "payAmountMinimal": "100500123",
      "payAmountDisplay": "100.500123",
      "payAddress": "TXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
      "chainId": 2,
      "tokenId": 1,
      "identify": {
        "mode": 1,
        "orderNo": "PO20261011XXXXXXXXXXXX",
        "calldataSuffix": "0x...order identify code..."
      }
    },
    "idempotentReplay": false
  },
  "msg": "下单成功"
}

Remember the three amounts:

  • amountMinimal — the amount you asked for, in the token's smallest unit.
  • payAmountMinimal — the amount that should actually be transferred. When the identification mode is "amount suffix" it carries an extra suffix. Always use this one.
  • payAmountDisplay — the same value, formatted for humans.

identify is this order's identification data. Pass it through verbatim — do not change its case and do not add a prefix, or the scanner will not recognise the order.

Step 3: The buyer pays ​

Give the buyer your payment counter link or QR code. The counter URL has this shape:

{cashierBaseUrl}/pay/cashier/m/{merchantNo}

Both cashierBaseUrl and merchantNo come from GET /api/pay/getMyProfile. Note that the merchant number is a path segment, not a query parameter — some wallet deep links truncate query strings at the =.

The checkout page then calls these public endpoints (no login required):

GET  /api/pay/getCashierMerchant     # display info such as the collection name
GET  /api/pay/getCashierChains       # available chains and tokens (sanitised)
GET  /api/pay/getCashierOrder        # look up one order by its order number
POST /api/pay/createCashierOrder     # in-person QR: buyer enters the amount first
POST /api/pay/submitCashierTxHash    # buyer reports the transaction hash after signing

If you pushed the order from your own system (an online order), send the buyer straight to the checkout page for getCashierOrder — there is no need to go through createCashierOrder.

Step 4: Receive the callback and update your order ​

Once confirmations are met, we POST to the configured address (or the notifyUrl given for this order). Verify the signature first, then act:

go
// Key point: the HMAC key is **your own API KEY**, and you sign **raw body bytes** —
// parse-then-reserialize will break it.
func verify(apiKey, ts, rawBody, sig string) bool {
    mac := hmac.New(sha256.New, []byte(apiKey))
    mac.Write([]byte(ts))
    mac.Write([]byte(rawBody))
    want := hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(want), []byte(sig))
}

func handler(w http.ResponseWriter, r *http.Request) {
    raw, _ := io.ReadAll(r.Body)
    ts := r.Header.Get("X-Pay-Timestamp")
    sig := r.Header.Get("X-Pay-Signature")
    if !verify(os.Getenv("PAY_API_KEY"), ts, string(raw), sig) {
        http.Error(w, "bad signature", http.StatusUnauthorized)
        return
    }
    var p struct {
        MerchantOrderNo string `json:"merchantOrderNo"`
        OrderNo         string `json:"orderNo"`
        Status          int    `json:"status"`
        ReceivedAmount  string `json:"receivedAmount"`
        TxHash          string `json:"txHash"`
    }
    _ = json.Unmarshal(raw, &p)

    // Idempotency: handle each merchantOrderNo once — callbacks are retried
    if alreadyDone(p.MerchantOrderNo) {
        w.WriteHeader(http.StatusOK) // only a 2xx stops the retries
        return
    }
    markPaid(p.MerchantOrderNo, p.TxHash, p.ReceivedAmount)
    w.WriteHeader(http.StatusOK)
}

After that:

  • Returning 2xx means you received it and we stop retrying.
  • Any other status (or no response) means we retry on a backoff schedule.
  • Never rely on callbacks alone — you can query the real state at any time.
bash
curl "$API_BASE/api/pay/open/queryOrder?merchantOrderNo=SHOP-2026-0001" \
  -H "X-Pay-Api-Key: $PAY_API_KEY"

That is one full payment. From here, pick the path that matches your setup:

Create order

Full detail on fields, amount conventions and the idempotency key.

Callbacks and verification

Retry schedule, idempotency, and why queries are authoritative.

Concepts

Every term above, collected in one place.

Web3 payment infrastructure for global merchants