Files
payouts/README.md
2026-04-02 19:31:09 +03:00

12 KiB

Payouts Service

A Go service for processing payouts via YooKassa, supporting SBP, YooMoney, bank card, and widget-based payout flows.


API Reference

GET /health

Health check endpoint. Verifies database connectivity.

Request parameters: None

Response:

Status Body
200 OK {"OK": true}
503 Service Unavailable {"OK": false, "Error": "error details"}

Example:

curl -s http://localhost:8080/health

GET /version

Returns the application version.

Request parameters: None

Response: Plain text version string (e.g. v1.0.0). If version is unknown, the git commit hash from build info is appended.

Example:

curl -s http://localhost:8080/version

POST /api/v1/user/register

Register a new user.

Request body (JSON):

Field Type Required Description
tin string yes Tax identification number
phone string yes Phone number (must be unique)
password string yes Password
password_cfm string yes Password confirmation (must match password)

Response:

Status Description
201 Created User registered successfully (no body)
400 Bad Request Validation error or phone already registered
500 Internal Server Error Password hashing failure

Example:

curl -s -X POST http://localhost:8080/api/v1/user/register \
  -H "Content-Type: application/json" \
  -d '{"tin":"123456789","phone":"+79001234567","password":"secret","password_cfm":"secret"}'

POST /api/v1/user/login

Authenticate a user and obtain a session token.

Request body (JSON):

Field Type Required Description
phone string yes Registered phone number
password string yes Password

Response (200 OK):

{
  "token": "550e8400-e29b-41d4-a716-446655440000",
  "token_ttl": 1712000000
}
Field Type Description
token string UUID session token to use in subsequent requests
token_ttl integer Unix timestamp when the token expires

Error response:

{
  "status": 401,
  "message": "Unauthorized"
}

Example:

curl -s -X POST http://localhost:8080/api/v1/user/login \
  -H "Content-Type: application/json" \
  -d '{"phone":"+79001234567","password":"secret"}'

GET /api/v1/payout/sbp/banks

Retrieve the list of banks available for SBP payouts.

Request parameters: None

Authentication: Not required

Response (200 OK):

{
  "type": "sbp_banks",
  "items": [
    {
      "bank_id": "100000000111",
      "name": "Тинькофф Банк",
      "bic": "044525974"
    }
  ]
}

Example:

curl -s http://localhost:8080/api/v1/payout/sbp/banks

POST /api/v1/payout/create

Create a payout. The payout_type determines which additional fields are required.

Request headers:

Header Required Description
Authorization yes Bearer {token} — session token from login
Content-Type yes application/json

Request body (JSON):

Field Type Required Description
payout_type string yes One of: spb, yoo_money, bank_card, widget
amount float yes Payout amount in rubles
payout_token string for widget Token received from the YooKassa widget success_callback
account_number string for yoo_money YooMoney wallet number or phone
bank_id string for spb Bank identifier from /api/v1/payout/sbp/banks
card_number string for bank_card Card number

Note: For spb, the phone number is taken from the authenticated user's profile.

Response (200 OK):

{
  "payout_id": "po-285e5ee7-0022-5000-8000-01516a44b37d",
  "payout_status": "succeeded"
}
Field Type Description
payout_id string YooKassa payout identifier
payout_status string One of: created, pending, succeeded, canceled, failed

Error response:

{
  "status": 401,
  "message": "Unauthorized"
}

Example (SBP payout):

curl -s -X POST http://localhost:8080/api/v1/payout/create \
  -H "Authorization: Bearer 550e8400-e29b-41d4-a716-446655440000" \
  -H "Content-Type: application/json" \
  -d '{"payout_type":"spb","amount":500.00,"bank_id":"100000000111"}'

Example (widget payout):

curl -s -X POST http://localhost:8080/api/v1/payout/create \
  -H "Authorization: Bearer 550e8400-e29b-41d4-a716-446655440000" \
  -H "Content-Type: application/json" \
  -d '{"payout_type":"widget","amount":500.00,"payout_token":"pt-285e5ee7-0022-5000-8000-01516a44b37d"}'

POST /api/v1/payout/callback

Webhook endpoint for YooKassa payout status notifications. Called by YooKassa when a payout status changes.

Note: When YooKassa.CheckAllowedCallbackAddress = true, requests are validated against a CIDR whitelist of YooKassa IP ranges.

Request body (JSON, sent by YooKassa):

{
  "id": "po-285e5ee7-0022-5000-8000-01516a44b37d",
  "status": "succeeded",
  "amount": {
    "value": "500.00",
    "currency": "RUB"
  },
  "payout_destination": {
    "type": "bank_card",
    "card": {
      "number": "220000******0001",
      "first6": "220000",
      "last4": "0001",
      "card_type": "MIR",
      "issuer_country": "RU",
      "issuer_name": "Sberbank"
    }
  },
  "description": "Payout description",
  "created_at": "2024-01-01T12:00:00.000Z",
  "succeeded_at": "2024-01-01T12:00:05.000Z",
  "test": false
}

Response: 200 OK (processing is asynchronous)

Example:

curl -s -X POST http://localhost:8080/api/v1/payout/callback \
  -H "Content-Type: application/json" \
  -d '{"id":"po-285e5ee7-0022-5000-8000-01516a44b37d","status":"succeeded","amount":{"value":"500.00","currency":"RUB"}}'

Payout Widget: /payout/widget

GET /payout/widget serves an HTML page that embeds the YooKassa Payout Widget. The widget collects card details from the user and returns a one-time payout_token that must be passed to /api/v1/payout/create.

Mobile App Integration

The widget page is designed to be loaded inside a WebView on Android or iOS. The widget communicates back to the native app via JavaScript bridge callbacks.

Widget Callbacks

The widget fires two callbacks:

success_callback(data) — called when the user successfully submits card details. The data object contains the payout_token and card metadata. See YooKassa widget output parameters.

error_callback(error) — called when an error occurs in the widget. See error output parameters.

Android

Expose a JavaScript interface named AndroidCallback on the WebView:

class AndroidBridge {
    @JavascriptInterface
    fun onWidgetData(dataJson: String) {
        val data = JSONObject(dataJson)
        val payoutToken = data.getString("payout_token")
        // Call /api/v1/payout/create with payout_type "widget"
        createPayout(payoutToken = payoutToken, amount = 500.0)
    }

    @JavascriptInterface
    fun onWidgetError(errorJson: String) {
        // Handle widget error
    }
}

webView.addJavascriptInterface(AndroidBridge(), "AndroidCallback")

iOS (WKWebView)

Add a WKScriptMessageHandler named iosCallback:

class WidgetMessageHandler: NSObject, WKScriptMessageHandler {
    func userContentController(
        _ userContentController: WKUserContentController,
        didReceive message: WKScriptMessage
    ) {
        guard let body = message.body as? String,
              let data = body.data(using: .utf8),
              let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
        else { return }

        if message.name == "onWidgetData" {
            let payoutToken = json["payout_token"] as? String ?? ""
            // Call /api/v1/payout/create with payout_type "widget"
            createPayout(payoutToken: payoutToken, amount: 500.0)
        } else if message.name == "onWidgetError" {
            // Handle widget error
        }
    }
}

let contentController = WKUserContentController()
let handler = WidgetMessageHandler()
contentController.add(handler, name: "onWidgetData")
contentController.add(handler, name: "onWidgetError")

Payout Flow After Widget Callback

When onWidgetData fires, call /api/v1/payout/create with payout_type = "widget" and the received payout_token:

curl -s -X POST https://your-service/api/v1/payout/create \
  -H "Authorization: Bearer {session_token}" \
  -H "Content-Type: application/json" \
  -d '{
    "payout_type": "widget",
    "amount": 500.00,
    "payout_token": "{payout_token_from_widget}"
  }'

Configuration

Configuration is loaded from a .properties file (default: config/payouts.properties).

YooKassa

Property Default Description
YooKassa.BaseUrl https://api.yookassa.ru/v3 YooKassa API base URL
YooKassa.Timeout 2s HTTP request timeout
YooKassa.Test false Enable test mode
YooKassa.ApiBaseKey Base API key (used for SBP bank list)
YooKassa.ApiBaseSecret Base API secret
YooKassa.ApiPayoutKey Payouts API key (gateway account ID; also used as account_id in the widget)
YooKassa.ApiPayoutSecret Payouts API secret
YooKassa.Retry.Enabled false Enable automatic request retries
YooKassa.Retry.Count 3 Total attempt count (including the initial request)
YooKassa.Retry.WaitTime 200ms Initial delay between retries
YooKassa.Retry.MaxWaitTime 5s Maximum delay (exponential backoff cap)
YooKassa.CheckAllowedCallbackAddress true Validate callback source IP against whitelist
YooKassa.AllowedCallbackSubnets YooKassa IP ranges Comma-separated CIDR subnets allowed to send callbacks
YooKassa.CallbackProcessTimeout 1s Timeout for async callback processing
YooKassa.WidgetVersion 3.1.0 YooKassa widget JS version loaded on /payout/widget

Database

Property Default Description
Database.Type Database driver: sqlite or postgres
Database.Connection Connection string. SQLite: payouts.db. PostgreSQL: host=127.0.0.1 user=gorm password=gorm dbname=gorm port=5432 sslmode=disable
Database.LogLevel Info GORM log level: Debug, Info, Warn, Error
Database.TraceRequests false Log all SQL queries

Session Cache

Property Default Description
Cache.TTL 24h Session token time-to-live (Go duration string)

Server

Property Default Description
Server.Port :8080 Listening address and port
Server.WriteTimeout 35s Response write timeout
Server.ReadTimeout 35s Request read timeout
Server.EnablePProfEndpoints false Expose /debug/pprof endpoints
Server.Tls.Enabled false Enable TLS
Server.Tls.CertFile Path to TLS certificate file
Server.Tls.KeyFile Path to TLS private key file

Logging

Property Default Description
Log.Level DEBUG Log level: DEBUG, INFO, WARN, ERROR
Log.FilePath ./logs/payouts.log Log file path
Log.TextOutput false Use plain text output instead of JSON
Log.StdoutEnabled true Write logs to stdout
Log.FileEnabled false Write logs to file