Eulen supports two methods of authenticating API requests:
1.
Client Credentials + short-lived access tokens — recommended for new integrations.
2.
Legacy API tokens — existing long-lived JWT tokens remain supported.
We recommend using the Client Credentials authentication method for all new integrations. The legacy API token method continues to work and there is currently no migration deadline.
Both methods end in the same place: an Authorization: Bearer <token> header on every API call. They differ in how you obtain that token and how long it lasts.
The recommended way to authenticate with the API is to create a client_id and client_secret pair through the Eulen Telegram Bot and exchange those credentials for a short-lived access token.Unlike the legacy API tokens, access tokens obtained through this method are valid for only a short period of time, reducing the impact of an accidentally exposed token.
Client credentials are created through the #Bot section of your private Telegram channel with Eulen.The syntax to create a new credential is:
/addclientcredentials <label> [<scope>]
label — required. A name of your choice used to identify the credential. Accepted characters are A-Z, a-z, 0-9, ., _ and -, with a maximum length of 64. Anything else — including accented letters and spaces — is rejected. Examples: production, backend-v2, payments.prod. A label already in use by a live credential is rejected.
scope — optional, defaults to all. The permissions granted to the credential. Available options are all, deposit, withdrawal or user.
/addclientcredentials production all
This creates a new credential with full API access.
The client_secret is displayed only once, when the credential is created. It cannot be retrieved later.If you lose the secret, you must revoke the credential and create a new one.
Keep the secret retrievable, not just deployed
A refresh session ends on its own after a long enough period, no matter how actively you renew it. When that happens the only way back is a fresh login with the client_id and client_secret, so store the secret somewhere your service can read it again — a secrets manager, not only an environment variable set once by hand.
withdrawal — Access to withdrawal-related operations.
user — Access to user-related operations.
all is a convenience for the command, not a scope in its own right: a credential created with all is granted deposit, withdrawal and user, and that is what you will see in the scope array of the login response and of the token itself.
The withdrawal scope is spelled differently in the two methods
Client Credentials use withdrawal. The legacy /apitoken command uses withdraw. Each command accepts only its own spelling — copying the scope across from a legacy token is rejected with Invalid scope.
Use the smallest scope required by your application. Avoid using all when the integration only requires access to specific API operations.
Revocation should be performed immediately if you believe a credential has been compromised or is no longer required.Revoking a credential ends every one of its refresh sessions at the same moment, so no further access tokens can be issued from it. Access tokens already issued keep working until they expire. Plan an incident response around that window rather than assuming revocation is instantaneous.
grant_type is accepted and ignored when its value is client_credentials, so OAuth-flavoured client libraries work unchanged. Any other value is rejected. No other field is accepted: an unrecognised key is a 400 that names the key, which is how a clientId / client_id mix-up surfaces immediately instead of as a puzzling authentication failure.The body must be JSON, at most 4 KiB, sent with Content-Type: application/json.
curl sends form encoding by default
curl -d '{...}' sets Content-Type: application/x-www-form-urlencoded and the request is rejected with 415. Use curl -H 'Content-Type: application/json' -d '{...}', or --json.
The RS256 JWT you send as Authorization: Bearer <token>
token_type
Always Bearer
expires_in
Lifetime of the access token, in seconds. Read this field — do not hard-code the number.
refresh_token
Opaque string used to obtain the next access token. Store it
refresh_expires_in
Seconds until the session ends from inactivity. Renewed on every rotation
scope
The scopes actually granted, as an array
Do not log in on every request
The access token is meant to be reused until it is close to expiry, and then refreshed — not re-obtained with the secret every time. Login is rate limited per credential and per IP; a service that logs in per API call will start receiving 429.
The response has exactly the same shape as the login response, including a newrefresh_token.Refresh tokens are single-use and rotating. Each call invalidates the token you presented and hands you its replacement. Always overwrite your stored refresh token with the one that just came back; the previous value is dead the moment the response is written.
Reusing a refresh token ends the whole session
Presenting a refresh token that has already been rotated is treated as a stolen-token replay: the entire session is revoked and a security alert is raised. Your service then has to log in again with the client_id and client_secret.This is the failure mode of a multi-process or multi-container deployment where several workers refresh independently from the same stored value. Serialise your refresh calls — a lock, a single dedicated renewer, or a shared cache with a compare-and-set — so that only one refresh is ever in flight for a session. Two simultaneous refreshes of the same token cannot be distinguished from an attack, and the safe interpretation is the hostile one.
A practical shape that avoids the problem entirely: renew ahead of expiry on a timer in one place, publish the resulting access token to your workers, and never let a worker refresh on its own 401.
Always answers 204, whether or not the token was still live — reporting "no such token" would turn logout into a way of testing whether a stolen token still works. The credential itself is untouched: you can log in with it again.
To authenticate with our API, use the HTTP header in the following format:
Authorization: Bearer <token>
Replacing <token> with the short-lived access token returned by /api/v2/auth/login.For example:
The client_secret is used to obtain an access token and should never be sent with normal API requests. Only send the resulting access token in the Authorization header.
The three authentication endpoints do not use the response envelope the rest of the API uses. They answer with a flat object and a machine-readable code:
{"error":"invalid_grant","errorMessage":"invalid or expired refresh token"}
Branch on error. errorMessage is there so existing error handling that already reads errorMessage keeps working.
error
Status
Meaning
invalid_request
400
Malformed body, unknown field, wrong JSON type, or a body over 4 KiB. The message names the specific problem
invalid_client
401
Login failed. Unknown, revoked, or wrong secret — deliberately indistinguishable
invalid_grant
401
Refresh failed. Unknown, expired, revoked, or replayed — deliberately indistinguishable
too_many_clients
409
The partner is at its credential limit
rate_limited
429
Too many attempts. Honour the Retry-After header
temporarily_unavailable
503
Try again shortly
server_error
500
Contact support with the time of the request
401 on /refresh is not something to retry with the same token: get a new session with POST /api/v2/auth/login.
Your credentials carry many privileges, so keep them secure!Never share your client_secret or access tokens in publicly accessible areas such as GitHub, client-side code, logs or similar locations.The client_secret should be stored securely, preferably using environment variables or a dedicated secrets manager.
If a credential is compromised, revoke it immediately:
/removeclientcredentials <label>
Then create a new credential with a new secret. Remember that access tokens already issued from the revoked credential remain valid until they expire.
Existing integrations can continue to use the legacy JWT authentication method.Authentication for legacy API access is performed using the JWT standard. To obtain a valid and properly signed JWT token, the partner must access the #Bot section of their private Telegram channel with Eulen.
The legacy API token remains supported. There is currently no migration deadline, and existing long-lived tokens continue to work normally.
The syntax to generate a new legacy token is as follows:
label - A completely arbitrary text of your choice, with no spaces, such as server001 or apitoken2024. This label will be used to manage your token later. You can invalidate your token using this label if it is no longer in use or has been compromised.
days - The number of days the token will remain valid, with 365 (one year) being the maximum.
scope - One of the following options: all, deposit, withdraw or user.
To authenticate with our API, use the HTTP header in the following format:
Authorization: Bearer <token>
Replacing <token> with the JWT token generated by our Telegram bot.Issuing a legacy token is self-service and immediate: a replacement, a rotation, or a narrower scope is another /apitoken command, at any time. You never need to request one from us.
Your credentials carry many privileges, so keep them secure! Never share your credential keys in publicly accessible areas such as GitHub, client-side code or similar locations.
GET /api/ping validates whichever token you send — legacy or short-lived — and echoes back its decoded claims under claims_for_token_debug, so it is the fastest way to confirm a token works and to see the scopes it actually carries.
Existing integrations using legacy API tokens do not need to migrate immediately.For new integrations, we recommend using the Client Credentials flow:
1.
Create a credential using /addclientcredentials.
2.
Store the client_id and client_secret securely, somewhere your service can read them again.
3.
Exchange the credentials for an access token using POST /api/v2/auth/login, and keep the refresh_token from the response.
4.
Use the returned access_token as Authorization: Bearer <token>.
5.
Before it expires, call POST /api/v2/auth/refresh from one place in your system, and replace your stored refresh token with the new one.
6.
If the credentials are compromised, revoke them using /removeclientcredentials.
The authentication header remains the same for both methods:
Authorization: Bearer <token>
The main difference is how the token is obtained: the new method uses short-lived access tokens renewed through the API, while the legacy method uses long-lived JWT tokens generated through the Telegram Bot.