Crypto Exchange API Development (REST, WebSocket)

We design and develop full-cycle blockchain solutions: from smart contract architecture to launching DeFi protocols, NFT marketplaces and crypto exchanges. Security audits, tokenomics, integration with existing infrastructure.
Showing 1 of 1 servicesAll 1306 services
Crypto Exchange API Development (REST, WebSocket)
Complex
~1-2 weeks
FAQ
Blockchain Development Services
Blockchain Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1214
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823

Crypto Exchange API Development (REST, WebSocket)

An exchange API is the public interface for traders, bots, and third-party integrations. Its quality determines the platform's attractiveness to professional participants. Binance, Kraken, OKX have standardized expectations: REST for order management and account, WebSocket for real-time data.

REST API Design

Endpoint Structure

Standard structure for public and private API:

Public API (no authentication):
GET  /api/v1/markets                     - list of trading pairs
GET  /api/v1/markets/{pair}/ticker       - current ticker
GET  /api/v1/markets/{pair}/orderbook    - order book snapshot
GET  /api/v1/markets/{pair}/trades       - recent trades
GET  /api/v1/markets/{pair}/candles      - OHLCV data

Private API (requires authentication):
GET    /api/v1/account/balances          - balances
GET    /api/v1/account/orders            - list of orders
POST   /api/v1/account/orders            - place order
DELETE /api/v1/account/orders/{id}       - cancel order
POST   /api/v1/account/orders/cancel-all - cancel all orders
GET    /api/v1/account/trades            - trade history
GET    /api/v1/account/deposits          - deposit history
POST   /api/v1/account/withdrawals       - create withdrawal

Authentication

Standard for crypto APIs — HMAC-SHA256 request signature:

// Server signature verification
func verifySignature(r *http.Request, secret string) bool {
    apiKey := r.Header.Get("X-API-Key")
    timestamp := r.Header.Get("X-Timestamp")
    signature := r.Header.Get("X-Signature")
    
    // Check timestamp (not older than 5 seconds — replay protection)
    ts, _ := strconv.ParseInt(timestamp, 10, 64)
    if time.Now().UnixMilli()-ts > 5000 {
        return false
    }
    
    // Signature string: method + path + timestamp + body
    method := r.Method
    path := r.URL.RequestURI()
    body, _ := io.ReadAll(r.Body)
    r.Body = io.NopCloser(bytes.NewBuffer(body)) // restore body
    
    message := method + path + timestamp + string(body)
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(message))
    expected := hex.EncodeToString(mac.Sum(nil))
    
    return hmac.Equal([]byte(signature), []byte(expected))
}

Client-side signature:

import hmac, hashlib, time, requests

def signed_request(method: str, path: str, body: dict = None):
    timestamp = str(int(time.time() * 1000))
    body_str = json.dumps(body) if body else ''
    
    message = method + path + timestamp + body_str
    signature = hmac.new(SECRET.encode(), message.encode(), hashlib.sha256).hexdigest()
    
    headers = {
        'X-API-Key': API_KEY,
        'X-Timestamp': timestamp,
        'X-Signature': signature,
        'Content-Type': 'application/json',
    }
    return requests.request(method, BASE_URL + path, headers=headers, data=body_str)

Response Format

Consistent format for all endpoints:

// Success
{
    "success": true,
    "data": { ... },
    "timestamp": 1700000000000
}

// Error
{
    "success": false,
    "error": {
        "code": "INSUFFICIENT_BALANCE",
        "message": "Insufficient balance to place order",
        "details": { "available": "0.5", "required": "1.0" }
    },
    "timestamp": 1700000000000
}

Standardized error codes are important for integrators. Better INSUFFICIENT_BALANCE than 400 Bad Request with text.

Rate Limiting

// Rate limiter by API key with bucket algorithm
type RateLimiter struct {
    redis *redis.Client
}

func (rl *RateLimiter) Check(apiKey string, weight int) error {
    key := "rate_limit:" + apiKey
    
    // Sliding window counter
    now := time.Now().UnixMilli()
    windowStart := now - 60000 // 60 seconds
    
    pipe := rl.redis.Pipeline()
    // Remove stale records
    pipe.ZRemRangeByScore(ctx, key, "0", strconv.FormatInt(windowStart, 10))
    // Add current request
    pipe.ZAdd(ctx, key, redis.Z{Score: float64(now), Member: fmt.Sprintf("%d-%d", now, rand.Int())})
    // Count sum of weights
    pipe.ZCard(ctx, key)
    pipe.Expire(ctx, key, 2*time.Minute)
    
    results, _ := pipe.Exec(ctx)
    count := results[2].(*redis.IntCmd).Val()
    
    // Different limits for different user levels
    limit := rl.getUserLimit(apiKey) // 1200 req/min for standard
    if int(count) > limit {
        return ErrRateLimitExceeded
    }
    return nil
}

Binance-style weight system: different endpoints have different "weights" (GET orderbook = 5, POST order = 1, GET all orders = 10). Allows flexible load management.

Pagination

For historical data — cursor-based pagination (better than offset for large tables):

type TradesQuery struct {
    Pair      string    `json:"pair"`
    Limit     int       `json:"limit"`  // max 1000
    StartTime *int64    `json:"start_time,omitempty"`
    EndTime   *int64    `json:"end_time,omitempty"`
    FromID    *string   `json:"from_id,omitempty"` // cursor
}

func (h *Handler) GetTrades(w http.ResponseWriter, r *http.Request) {
    q := parseTradesQuery(r)
    
    trades, err := h.db.GetTrades(q)
    
    var nextCursor *string
    if len(trades) == q.Limit {
        lastID := trades[len(trades)-1].ID
        nextCursor = &lastID
    }
    
    writeJSON(w, map[string]interface{}{
        "trades":      trades,
        "next_cursor": nextCursor,
        "has_more":    nextCursor != nil,
    })
}

WebSocket API

WebSocket Server Architecture

type WSHub struct {
    clients    map[*WSClient]bool
    subscriptions map[string]map[*WSClient]bool  // channel -> clients
    broadcast  chan WSMessage
    register   chan *WSClient
    unregister chan *WSClient
    mu         sync.RWMutex
}

func (h *WSHub) Run() {
    for {
        select {
        case client := <-h.register:
            h.mu.Lock()
            h.clients[client] = true
            h.mu.Unlock()
            
        case client := <-h.unregister:
            h.mu.Lock()
            delete(h.clients, client)
            for _, subs := range h.subscriptions {
                delete(subs, client)
            }
            h.mu.Unlock()
            
        case message := <-h.broadcast:
            h.mu.RLock()
            for client := range h.subscriptions[message.Channel] {
                select {
                case client.send <- message.Data:
                default:
                    // Client slow, buffer full — drop
                    close(client.send)
                    delete(h.clients, client)
                }
            }
            h.mu.RUnlock()
        }
    }
}

Subscription Protocol

// Client → Server: subscription
{"op": "subscribe", "channels": ["ticker.BTC-USDT", "orderbook.ETH-USDT.50", "trades.BTC-USDT"]}

// Server → Client: confirmation
{"op": "subscribed", "channels": ["ticker.BTC-USDT", "orderbook.ETH-USDT.50", "trades.BTC-USDT"]}

// Server → Client: ticker data
{
    "channel": "ticker.BTC-USDT",
    "data": {
        "pair": "BTC-USDT",
        "last": "42150.50",
        "bid": "42148.00",
        "ask": "42152.00",
        "volume_24h": "1234.56",
        "change_24h": "+2.35",
        "ts": 1700000000000
    }
}

// Order book diff update
{
    "channel": "orderbook.ETH-USDT.50",
    "type": "diff",
    "seq": 12345,
    "data": {
        "bids": [["2200.00", "5.5"], ["2195.00", "0"]],
        "asks": [["2205.00", "3.2"]]
    }
}

WebSocket Authentication

func (ws *WSClient) HandleAuth(msg AuthMessage) {
    // Verify signature like in REST
    if !verifyWSAuth(msg.APIKey, msg.Timestamp, msg.Signature) {
        ws.sendError("AUTH_FAILED", "Invalid signature")
        return
    }
    
    ws.userID = getUserIDByAPIKey(msg.APIKey)
    ws.authenticated = true
    ws.sendSuccess("authenticated")
}

// After authorization, access to private channels
// {"op": "subscribe", "channels": ["orders", "trades", "balances"]}

Heartbeat

func (ws *WSClient) startPingPong() {
    ticker := time.NewTicker(30 * time.Second)
    defer ticker.Stop()
    
    for {
        select {
        case <-ticker.C:
            ws.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
            if err := ws.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
                ws.close()
                return
            }
        case <-ws.done:
            return
        }
    }
}

Client must respond to Ping within 10 seconds. If not — connection closes. Clients can also send {"op": "ping"} to keep connection alive.

API Documentation

OpenAPI 3.0 + Swagger UI — standard for REST documentation. Documentation generated from code annotations or written manually:

# openapi.yaml
paths:
  /api/v1/account/orders:
    post:
      summary: Place Order
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PlaceOrderRequest'
            example:
              pair: "BTC-USDT"
              side: "buy"
              type: "limit"
              quantity: "0.001"
              price: "42000"
              time_in_force: "GTC"

WebSocket documentation — separate page describing subscription protocol, message formats, and code examples (Python, JavaScript, Go).

SDKs for Major Languages

Public API without SDKs — additional integration barrier. Minimum set:

# Python SDK
class ExchangeClient:
    def __init__(self, api_key: str, api_secret: str, testnet: bool = False):
        self.base_url = 'https://testnet-api.exchange.com' if testnet else 'https://api.exchange.com'
        self.api_key = api_key
        self.api_secret = api_secret
        self.session = aiohttp.ClientSession()
    
    async def place_order(self, pair: str, side: str, type: str, 
                           quantity: str, price: str = None) -> dict:
        return await self._post('/api/v1/account/orders', {
            'pair': pair, 'side': side, 'type': type,
            'quantity': quantity, 'price': price,
        })
    
    # WebSocket
    async def subscribe_ticker(self, pairs: list[str], callback: Callable):
        async with websockets.connect(self.ws_url) as ws:
            await ws.send(json.dumps({
                'op': 'subscribe',
                'channels': [f'ticker.{p}' for p in pairs]
            }))
            async for msg in ws:
                await callback(json.loads(msg))

Testing

Testnet is mandatory. Separate infrastructure with same endpoints but test currency. Binance, OKX, Kraken — all provide testnet.

Load testing: k6 or wrk for REST (goal — 10,000 req/sec at p99 latency < 50ms), artillery for WebSocket (1000+ concurrent connections, check broadcast latency).

Timeline

Component Timeline
Public REST API (5–7 endpoints) 3–4 weeks
Private REST API + HMAC auth 3–4 weeks
WebSocket server + subscriptions 3–4 weeks
Rate limiting + API key management 1–2 weeks
OpenAPI documentation 1 week
Python SDK 1–2 weeks
Load testing 1–2 weeks

Complete API with documentation, SDK, and testing: 3–4 months.