Self-Hosted API: All POST/PUT/DELETE Endpoints Return 403 Forbidden (GET Works, Webhooks Work)

Hi Gainium Team,

I’m running Gainium Self-Hosted (latest version via docker-sh) and building an integration with a Python-based trading automation system. I’ve encountered a specific issue with API authentication for write operations.

Environment

  • Self-Hosted Gainium via Docker (gainium-app-api-1:7503, gainium-app-frontend-1:7500)

  • Nginx Proxy Manager with Let’s Encrypt SSL

  • API URL:

    https://api.[my-domain]/api
    
  • Python 3.x with

    httpx
    

    for HTTP requests


The Issue

All write operations (POST/PUT/DELETE) return

403 Forbidden

, while read operations (GET) and webhooks work correctly.

Request Type Endpoint Result
GET
/api/bots/dca
:white_check_mark: 200 OK - Returns bot list
GET
undefined
/api/exchanges
:white_check_mark: 200 OK
POST
undefined
/trade_signal

(Webhook)

:white_check_mark: 200 OK - Bot starts/stops
POST
undefined
/api/startBot?botId=XXX&type=dca
:cross_mark: 403 Forbidden
POST
undefined
/api/updateDCABot?botId=XXX
:cross_mark: 403 Forbidden
PUT
undefined
/api/cloneDCABot?botId=XXX
:cross_mark: 403 Forbidden
DELETE
undefined
/api/stopBot?botId=XXX
:cross_mark: 403 Forbidden

What I’ve Verified

1. Signature Algorithm is Correct

I’m using the exact algorithm from the Swagger docs:

python

prehash = body + method + endpoint + timestamp
signature = base64.b64encode(
hmac.new(secret.encode(), prehash.encode(), hashlib.sha256).digest()
).decode()

Proof: GET requests use the same signature logic and work perfectly.

2. Tested Multiple Prehash Formats

I tried both:

  • Without query params:

    POST/api/startBot1766513277266
    
  • With query params:

    POST/api/startBot?botId=XXX1766513277266
    

Both return 403.

3. Tested POST With and Without Body

  • POST /api/startBot
    

    (no body) → 403

  • POST /api/updateDCABot
    

    (with JSON body) → 403

4. Headers Are Correct

token: [API_KEY]

time: [TIMESTAMP_MS]

signature: [BASE64_SIGNATURE]

Content-Type: application/json


Debug Output

🔐 Testing POST /api/startBot (no body)

Prehash: POST/api/startBot1766513277266

URL: https://api.[my-domain]/api/startBot?botId=[BOT_ID]&type=dca

:inbox_tray: Response:

Status: 403

Body: Forbidden

For comparison, this GET request works with identical signature logic:

Prehash: GET/api/bots/dca1766507695223

Status: 200 OK

Body: {“status”: “OK”, “data”: {“result”: […]}}


My Questions

  1. Are write endpoints (POST/PUT/DELETE) disabled by default on Self-Hosted?
    If so, is there a configuration flag in

    .env
    

    or

    docker-compose.yml
    

    to enable them?

  2. Does the API key require specific permissions for write operations?
    I created the key with “Write” permissions in the UI, but perhaps there’s an additional scope required?

  3. Is there a difference between Cloud and Self-Hosted API capabilities?
    The Swagger docs show these endpoints exist, but they seem inaccessible.

  4. As a workaround: Can webhooks be extended to support

    updateSettings
    

    or

    changePair
    

    actions?
    Currently webhooks only support:

    startBot
    

    ,

    stopBot
    

    ,

    addFunds
    

    ,

    reduceFunds
    

    .


What I’m Trying to Achieve

Building a system that:

  1. Clones a template bot

  2. Updates its settings (pair, TP%, step, etc.) dynamically

  3. Starts/stops based on market conditions

Currently I can only use webhooks for start/stop, but cannot update settings or clone bots programmatically.

Any guidance would be greatly appreciated. Happy to provide additional logs or test specific configurations.

Thanks!

Hello! Thank you for reporting this detailed issue regarding the Self-Hosted API POST/PUT/DELETE endpoints returning a 403 Forbidden error. We have received your report and will update you shortly.

You’ve provided a lot of helpful information already. Just to confirm for our investigation, could you please provide the botId for the bots you are trying to interact with using the API? This will help us trace the issue more effectively.

UPDATE: Solved! :tada:

After extensive debugging, I found the root cause of both the 403 Forbidden and 401 Invalid Signature errors.

There are two critical undocumented requirements for the Gainium API signature:


1. Query Parameters MUST be included in the prehash

The endpoint string in the prehash must include the full path with query parameters, not just the base endpoint:

:cross_mark: WRONG:

javascript

prehash = body + "POST" + "/api/updateDCABot" + timestamp

:white_check_mark: CORRECT:

javascript

prehash = body + "POST" + "/api/updateDCABot?botId=abc123" + timestamp

2. JSON Body MUST be compact (no spaces after colons)

The JSON body used in both the request AND signature must have no spaces:

:cross_mark: WRONG:

json

{"tpPerc": "1.5", "name": "My Bot"}

:white_check_mark: CORRECT:

json

{"tpPerc":"1.5","name":"My Bot"}

In Python:

python

body = json.dumps(data, separators=(',', ':'))

Working Python Example

python

import hashlib, hmac, base64, json, time

bot_id = "your_bot_id"
timestamp = str(int(time.time() * 1000))

# Compact JSON (no spaces!)
body = json.dumps({"tpPerc": "1.5"}, separators=(',', ':'))

# Endpoint WITH query params
endpoint = f"/api/updateDCABot?botId={bot_id}"

# Prehash
prehash = f"{body}POST{endpoint}{timestamp}"

signature = base64.b64encode(
    hmac.new(API_SECRET.encode(), prehash.encode(), hashlib.sha256).digest()
).decode()

This fixed all 403 and 401 errors for me. Works for both Paper and Live bots (&paperContext=true for Paper).

Hope this helps others! Maybe it could be added to the official API documentation.

1 Like

Please use

```python
# This is my
# multi-line code
a = 'b'

```

to get appropriate code blocks then it will look like

# This is my
# multi-line code
a = 'b'

Thanks for the update! Glad you found the solution, I will be updating the docs to make it clearer.

1 Like

It was a quick post without fixing the code blocks, but I fixed it now, I think.