Skip to content

Authentication

Besides login itself, every query and mutation requires authentication.

Authentication is handled with the login mutation. It returns a JWT which you then send on every subsequent request in the authorization header.

1. Request a token

graphql
mutation Login($email: String!, $password: String!) {
  login(email: $email, password: $password) {
    token
    user {
      id
      email
      first_name
      last_name
      plan
      signup_finished
      artists {
        id
        name
        spotify_id
        avatar_url
        username
        plan
        active_plan
      }
      feature_flags {
        feature
        enabled
      }
    }
  }
}

Variables:

json
{ "email": "user@domain.com", "password": "secure_password" }

Response:

json
{
  "data": {
    "login": {
      "token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9...",
      "user": {
        "id": "8f2c1e7a-...",
        "email": "user@domain.com",
        "artists": [
          { "id": "3aad8009-a307-4429-a586-8b3dbe39cdda", "name": "Example Artist" }
        ]
      }
    }
  }
}

The user.artists array is the authoritative list of artists this account can act on. Save those IDs — most other operations need one.

2. Send the token

authorization: Bearer eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9...
bash
curl https://api.artisthub.io/v1/graphql \
  -H 'content-type: application/json' \
  -H "authorization: Bearer $TOKEN" \
  -d '{"query":"{ artists { id name } }"}'

Token format

Tokens are JSON Web Tokens signed with RS512. The payload carries a standard Hasura claims object:

json
{
  "https://hasura.io/jwt/claims": {
    "x-hasura-allowed-roles": ["manager"],
    "x-hasura-default-role": "manager",
    "x-hasura-user-id": "8f2c1e7a-..."
  }
}

You do not need to parse the token — but if you do, x-hasura-user-id is the ID of the authenticated user, and it is what every row-level permission rule is evaluated against.

WARNING

Tokens are currently issued without an expiry claim, so a token stays valid until the account is disabled or its password changes. Treat it as a long-lived credential: store it in a secret manager, never commit it, never ship it in client-side code, and re-issue it by calling login again if you suspect it has leaked.

The refresh_token mutation exists in the schema but is not yet implemented — it returns { "token": null }. Call login again instead.

Roles

Every request runs under a role, which decides both which fields you can see and which rows within them.

RoleHow you get itWhat it can do
managerIssued by loginFull read/write on the artists this user is attached to. This is the role for all API integrations.
anonymousNo tokenPublic blog content only. All artist, link, and analytics fields are hidden.
fanFan-facing flowsA fan's own presave records. Not used by API integrations.
selfInternalA narrow view of the user's own record.

As a manager, your visibility is scoped by ownership. Reading links does not return every link in ArtistHub — it returns links whose artist you are attached to. The rule applied is effectively:

json
{ "artist": { "users": { "user_id": { "_eq": "X-Hasura-User-Id" } } } }

This is enforced by the server, so you can safely query links with no where clause and receive only your own rows. It also means a query for a link you do not own returns an empty array rather than an error.

Errors

All authentication failures return HTTP 200 with an errors array.

invalid-jwt

The token is malformed, truncated, or not signed by ArtistHub.

json
{
  "errors": [
    {
      "extensions": { "code": "invalid-jwt", "path": "$" },
      "message": "Could not verify JWT: JWSError JWSInvalidSignature"
    }
  ]
}

Common causes: the Bearer prefix was omitted, the header was truncated, or a token from a different environment was used. Request a fresh token with login.

validation-failed — "field not found"

json
{
  "errors": [
    {
      "extensions": { "code": "validation-failed", "path": "$.selectionSet.artists" },
      "message": "field 'artists' not found in type: 'query_root'"
    }
  ]
}

This is the error you get when the authorization header is missing entirely. Without a token the request runs as anonymous, and the schema exposed to anonymous does not contain artists — so the field genuinely does not exist for that role. Check that the header is being sent before you go looking for a typo.

Invalid email or password.

json
{
  "data": null,
  "errors": [
    {
      "extensions": { "code": "INTERNAL_SERVER_ERROR" },
      "message": "Invalid email or password.",
      "path": ["login"]
    }
  ]
}

Returned by login for both an unknown email and a wrong password — the two cases are deliberately indistinguishable.

This account is disabled.

The account exists but has been deactivated. Contact support; no token will be issued.

MutationPurpose
reset_password_request(email: String!)Emails a password-reset link.
reset_password(token: String!, password: String!)Completes a reset using the emailed token.
update_password(current_password: String, new_password: String!, user_id: String)Changes the password for a signed-in user.
update_email_request(email: String!)Starts an email change; sends a confirmation token.
update_email(email: String!, email_token: String!)Completes the email change.
verify_email(email: String!, email_token: String!)Verifies a newly registered address.
get_current_user_data (query)Re-reads the same user payload login returns, without re-authenticating.

get_current_user_data is the cheapest way to check whether a stored token is still good.

ArtistHub developer documentation