Skip to content

Authentication

Every request carries a bearer token in the Authorization header:

Authorization: Bearer <token>

For your backend, the token is a session token for one staff member. You get it by signing that person in with their email, password and a code from their authenticator app. The person must supply the code each time they sign in.

Signing in takes two calls.

POST /api/v1/auth/sign-in takes email and password:

Terminal window
jq -n '{email: env.ELE_EMAIL, password: env.ELE_PASSWORD}' |
curl -s "$ELE_API_BASE/api/v1/auth/sign-in" \
-H 'Content-Type: application/json' \
--data @-

jq reads the credentials from the environment and encodes them as JSON, so a password with quotes or backslashes still works. --data @- reads the body from standard input, so the password does not appear in the process list.

A wrong email or password returns 401 with the error type invalid_credentials. Otherwise the response is 200, and its status says what happens next:

status Meaning What to do
mfa_required The password was right. Ele needs a code from the person’s authenticator app. Send the code with the challenge_token (step 2).
authenticated Signed in. The response has a session token. Use the token.
totp_setup_required An organization the person belongs to requires an authenticator app, and they have not set one up. See Setting up an authenticator app.
webauthn_required The account has no authenticator app, and this device holds a passkey for it. Not possible from a server. See Passkeys.

authenticated comes straight back when the account has no authenticator app, so the password alone signs it in. Do not run staff accounts like that: give every staff member an authenticator app.

POST /api/v1/auth/mfa takes the challenge_token and the person’s current code:

Terminal window
curl -s "$ELE_API_BASE/api/v1/auth/mfa" \
-H 'Content-Type: application/json' \
-d "{\"challenge_token\": \"$CHALLENGE_TOKEN\", \"code\": \"$ELE_TOTP_CODE\"}"

The response is 200, and its status is one of:

status Meaning What to do
authenticated Signed in. The response has a session token and the user. Use the token.
mfa_failed The code was wrong, or was already used. Ask the person for a new code and send it with the same challenge_token.
challenge_expired The challenge token is too old or no longer valid. Start again from step 1.

A challenge token lasts 10 minutes. Each authenticator code works only once, so if the person has just used a code, they must wait for the next one. After too many wrong codes, a challenge token keeps returning mfa_failed, even for a right code. Start again from step 1.

This program signs a person in and prints the session token:

signin/main.go
// Sign-in: sign a staff member in with their password and an authenticator
// code, and print the session token.
//
// export ELE_TOKEN=$(ELE_EMAIL=… ELE_PASSWORD=… ELE_TOTP_CODE=123456 go run ./signin)
//
// The token is the only thing written to standard output, so a shell can
// capture it. Everything else goes to standard error. The program refuses to
// print the token to a terminal, where it would stay in the scrollback. In
// your own backend, keep the token in your secret store, never in logs.
//
// Set ELE_DEVICE_ID to the device id from an earlier sign-in to sign in as
// that device rather than a new one.
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/elephants-labs/ele/developers/examples/internal/ele"
)
// signInResult covers both the sign-in and the MFA responses. Status says
// which of the other fields are set.
type signInResult struct {
Status string `json:"status"`
Token string `json:"token"`
ChallengeToken string `json:"challenge_token"`
}
func main() {
if stat, err := os.Stdout.Stat(); err == nil && stat.Mode()&os.ModeCharDevice != 0 {
log.Fatal("refusing to print a session token to a terminal; capture it instead: export ELE_TOKEN=$(go run ./signin)")
}
ctx := context.Background()
c := ele.FromEnv()
body := map[string]string{
"email": os.Getenv("ELE_EMAIL"),
"password": os.Getenv("ELE_PASSWORD"),
}
if id := os.Getenv("ELE_DEVICE_ID"); id != "" {
body["device_id"] = id
}
var res signInResult
err := c.Do(ctx, "POST", "/api/v1/auth/sign-in", body, &res)
if err != nil {
log.Fatal(err)
}
if res.Status == "mfa_required" {
// The challenge token lasts 10 minutes. On "mfa_failed" you may ask
// the person for a new code and send it with the same challenge token.
err := c.Do(ctx, "POST", "/api/v1/auth/mfa", map[string]string{
"challenge_token": res.ChallengeToken,
"code": os.Getenv("ELE_TOTP_CODE"),
}, &res)
if err != nil {
log.Fatal(err)
}
}
switch res.Status {
case "authenticated":
fmt.Println(res.Token)
case "mfa_failed":
log.Fatal("the authenticator code was wrong or already used; wait for the next code and try again")
case "challenge_expired":
log.Fatal("the sign-in challenge expired; start again")
case "totp_setup_required":
log.Fatal("an organization this person belongs to requires an authenticator app, and they have not set one up yet")
case "webauthn_required":
log.Fatal("this account has no authenticator app and this device holds a passkey, which only works on Ele's own apps; add an authenticator app to the account")
default:
log.Fatalf("unexpected sign-in status %q", res.Status)
}
}

Run it from the examples directory:

Terminal window
export ELE_TOKEN=$(ELE_TOTP_CODE=123456 go run ./signin)

It reads ELE_EMAIL and ELE_PASSWORD from the environment. It refuses to print the token to a terminal, so capture it as shown.

If sign-in returns totp_setup_required, the person can set up an authenticator app as part of signing in:

  1. Call POST /api/v1/auth/sign-in/totp/setup with the challenge_token. It returns uri and secret_encoded.
  2. Show the uri to the person, for example as a QR code, so they can add it to their own authenticator app.
  3. Call POST /api/v1/auth/sign-in/totp/activate with the challenge_token and a code from the app. It answers like the MFA call above.

The secret belongs in the person’s authenticator app and nowhere else. Do not store it, log it or keep it on your server.

A person who is already signed in can add an authenticator app with POST /api/v1/auth/totp/setup and POST /api/v1/auth/totp/activate. The activate call needs both a code from the app and a WhatsApp code that Ele sends during setup.

A passkey is bound to Ele’s own web address. Browsers and phones will only use it on Ele’s sites and apps, so your server cannot complete a passkey sign-in. If sign-in returns webauthn_required, add an authenticator app to that account. When an account has an authenticator app, sign-in always asks for its code instead of a passkey.

Each sign-in belongs to a device. If you do not send device_id, Ele records a new device for every sign-in, and may notify the person about a sign-in from a new device.

To avoid that, after the first sign-in call GET /api/v1/devices and keep the id of the entry with this_device: true. Send it as device_id the next time that person signs in from your server.

The sign-in example sends ELE_DEVICE_ID as device_id when you set it.

Ele expects a person to be present when they sign in and when they approve something. Some operations exist for Ele’s own mobile apps, which keep a credential behind the phone’s lock. Called from a server, they would let that server sign in or approve payments with no person present. Do not use them from a server:

  • Device trust. Leave remember_me, trust_transport and trust_tokens unset when you sign in. With them, Ele issues a token that lets later sign-ins skip the authenticator code for 30 days.
  • Device login credentials: /api/v1/auth/login-credential and /api/v1/auth/sign-in/device. They sign a person in without a password or a code.
  • Approval credentials and challenges: /api/v1/me/approval-credential and the /api/v1/me/challenges operations. They let a phone answer step-up for the person, including payment approvals.

Do not register your server as one of the person’s phones by any other route either. Ele’s rule is that a person approves each payment, and these pages ask you to keep it that way.

Send the token on every request:

Terminal window
curl -s "$ELE_API_BASE/api/v1/me" \
-H "Authorization: Bearer $ELE_TOKEN"

A session ends at whichever comes first:

  • 30 minutes without use. Each authenticated request moves the end to 30 minutes after that request.
  • 30 days after sign-in. Use does not extend this limit.

When a session has ended, every request with its token returns 401:

{
"error": {
"type": "not_authenticated",
"message": "Unauthorized",
"request_id": "…"
}
}

A session cannot be extended past 30 days. To continue, sign the person in again, which needs a new code from them. Plan your product so a staff member can sign in again when their session ends.

POST /api/v1/auth/sign-out ends the session whose token you send:

Terminal window
curl -s -X POST "$ELE_API_BASE/api/v1/auth/sign-out" \
-H "Authorization: Bearer $ELE_TOKEN"

It always returns 200 with {"message": "goodbye"}, even when the token has already expired. Call it when a staff member signs out of your product, and then delete your copy of the token.

A session token acts as the person until it ends. Treat it like a password:

  • Keep it in a secret store or encrypted storage, never in source code.
  • Never write it to logs, error reports or analytics.
  • Keep one token per staff member, and never share a token between people.
  • Never store a staff member’s password or authenticator secret.