> ## Documentation Index
> Fetch the complete documentation index at: https://docs.riseworks.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> SIWE authentication for these endpoints

This uses the same Sign-In with Ethereum (SIWE) flow as the original V1 API: request a message, sign it with your wallet, then exchange the signature for a JWT.

<Steps>
  <Step title="Get SIWE message">
    `GET /v1/auth/api/siwe` returns a message for your wallet to sign.
  </Step>

  <Step title="Sign with wallet">
    Sign the returned message using your wallet's private key.
  </Step>

  <Step title="Verify and get JWT">
    `POST /v1/auth/api/siwe` verifies the signature and returns a JWT valid for 24 hours.
  </Step>
</Steps>

## Which flow do I use?

<CardGroup cols={2}>
  <Card title="Personal auth" icon="user">
    Pass **`wallet` only**. Use this when the signing wallet belongs to an individual Rise user who already has admin access on one or more teams/companies.
  </Card>

  <Card title="Company/team-scoped auth" icon="building">
    Pass **`wallet` + `rise_id` + `impersonate`**. This is the standard pattern for API integrations: a dedicated wallet, registered as the team/company's **Owner** or a **delegate**, authenticates *as* a specific manager on that team/company.
  </Card>
</CardGroup>

Both flows use the same two endpoints and response shapes below — they differ only in which query params you send on the `GET` step, since those params get embedded as `resources` inside the message you sign.

For the company/team-scoped flow, two identities are checked independently:

* **`wallet`** must be the team/company's on-chain **Owner**, or a wallet added as a **delegate** on that RiseID (see [Secondary Wallets](/authentication/secondary-wallets) for setting one up). This proves the *caller* is authorized to act for the entity.
* **`impersonate`** must be the email of an existing Rise user who is a **manager** on that same team — specifically one with the `team_admin`, `team_finance_admin`, `team_delegate_admin`, or `team_payment_initiator` role. This determines *whose identity* (and payroll permissions) the resulting JWT carries. It does not need to be the same person as the delegate wallet's owner.

## Get SIWE message

**GET** `/v1/auth/api/siwe`

<ParamField query="wallet" type="string" required>
  Wallet address doing the signing.
</ParamField>

<ParamField query="rise_id" type="string">
  The **team or company's** V1 RiseID you're authenticating against — not a personal user RiseID. Required together with `impersonate` for the company/team-scoped flow.
</ParamField>

<ParamField query="impersonate" type="string">
  Email of the manager to authenticate as within that team/company — must be a user with the `team_admin`, `team_finance_admin`, `team_delegate_admin`, or `team_payment_initiator` role on `rise_id`. Required together with `rise_id`.
</ParamField>

<RequestExample>
  ```bash theme={null}
  # Personal auth
  curl -X GET "https://b2b-api.riseworks.io/v1/auth/api/siwe?wallet=0xYourWalletAddress"

  # Company/team-scoped auth
  curl -X GET "https://b2b-api.riseworks.io/v1/auth/api/siwe?wallet=0xYourWalletAddress&rise_id=0xTeamOrCompanyRiseId&impersonate=manager@example.com"
  ```
</RequestExample>

<ResponseExample>
  ```json theme={null}
  {
    "data": {
      "wallet": "0xYourWalletAddress",
      "message": "riseworks.io wants you to sign in with your Ethereum account:\n0xYourWalletAddress\n\n..."
    }
  }
  ```
</ResponseExample>

<Note>
  When `rise_id`/`impersonate` are supplied, the returned `message` embeds them as SIWE `resources`. Sign that exact message as-is — don't reconstruct or strip the resources before signing, or verification will fail.
</Note>

## Verify SIWE signature

**POST** `/v1/auth/api/siwe`

<ParamField body="wallet" type="string" required>
  Wallet address of the user.
</ParamField>

<ParamField body="message" type="string" required>
  The original SIWE message returned by the GET step.
</ParamField>

<ParamField body="signature" type="string" required>
  The signature produced by signing `message` with the wallet's private key.
</ParamField>

<RequestExample>
  ```bash theme={null}
  curl -X POST "https://b2b-api.riseworks.io/v1/auth/api/siwe" \
    -H "Content-Type: application/json" \
    -d '{
      "wallet": "0xYourWalletAddress",
      "message": "riseworks.io wants you to sign in with your Ethereum account:...",
      "signature": "0xSignatureFromWallet"
    }'
  ```
</RequestExample>

<ResponseExample>
  ```json theme={null}
  {
    "data": {
      "token": "eyJhbGciOiJIUzI1NiIs...",
      "expires_in": 86400,
      "wallet_address": "0xYourWalletAddress"
    }
  }
  ```
</ResponseExample>

Use the returned `token` as a bearer token on every other request:

```bash theme={null}
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
```

## Creating the signature and token in code

### Using the SDK (recommended)

**Personal auth** — `riseIdAuth` configures the client to authenticate (and re-authenticate on expiry) automatically. `riseId` here must be the individual user's own personal RiseID, not a team/company one:

```javascript theme={null}
const { RiseApiClient } = require('riseworks-sdk');

const client = new RiseApiClient({
  environment: 'prod',
  riseIdAuth: {
    riseId: process.env.RISE_ID, // your personal RiseID
    privateKey: process.env.PRIVATE_KEY,
  },
});

// The client lazily generates/refreshes the JWT on the first call.
const teams = await client.v1Legacy.listTeams();
```

**Company/team-scoped auth** — `riseIdAuth`'s automatic flow doesn't apply here (it's V2-only and expects a personal RiseID). Call `getSiwe`/`verifySiwe` yourself, passing the team/company's `rise_id`, then hand the resulting token to the client with `updateToken`:

```javascript theme={null}
const { RiseApiClient } = require('riseworks-sdk');
const { ethers } = require('ethers');

// jwtToken is required to construct a client, but any placeholder works here —
// updateToken() below replaces it before any authenticated call is made.
const client = new RiseApiClient({ environment: 'prod', jwtToken: 'pending' });

const wallet = new ethers.Wallet(process.env.WALLET_PRIVATE_KEY);
const teamOrCompanyRiseId = process.env.TEAM_OR_COMPANY_RISE_ID;

const { data } = await client.v1Legacy.getSiwe({
  wallet: wallet.address,
  rise_id: teamOrCompanyRiseId,
  impersonate: 'manager@example.com', // must have a manager role on rise_id
});
const signature = await wallet.signMessage(data.message);
const { data: tokenData } = await client.v1Legacy.verifySiwe({
  wallet: wallet.address,
  message: data.message,
  signature,
});

client.updateToken(tokenData.token);

// Every subsequent client.v1Legacy.* call is now authenticated as the
// impersonated user within teamOrCompanyRiseId.
const teams = await client.v1Legacy.listTeams();
```

### Manual HTTP (no SDK)

```javascript theme={null}
import { ethers } from 'ethers';

const baseUrl = 'https://b2b-api.riseworks.io';
const wallet = new ethers.Wallet(process.env.WALLET_PRIVATE_KEY);

async function authenticate() {
  // Step 1: Get the SIWE message to sign
  const siweResponse = await fetch(
    `${baseUrl}/v1/auth/api/siwe?wallet=${wallet.address}`
  );
  const { data } = await siweResponse.json();
  const { message } = data;

  // Step 2: Sign the message with your wallet
  const signature = await wallet.signMessage(message);

  // Step 3: Verify the signature and receive a JWT
  const verifyResponse = await fetch(`${baseUrl}/v1/auth/api/siwe`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      wallet: wallet.address,
      message,
      signature,
    }),
  });
  const { data: tokenData } = await verifyResponse.json();

  // tokenData.token is valid for tokenData.expires_in seconds (86400 = 24h)
  return tokenData.token;
}

const token = await authenticate();

// Use the token on every subsequent request
const teamsResponse = await fetch(`${baseUrl}/v1/teams`, {
  headers: { Authorization: `Bearer ${token}` },
});
```

For the company/team-scoped flow, add `rise_id`/`impersonate` to the `GET` query string:

```javascript theme={null}
const siweResponse = await fetch(
  `${baseUrl}/v1/auth/api/siwe?wallet=${wallet.address}&rise_id=${teamOrCompanyRiseId}&impersonate=${encodeURIComponent('manager@example.com')}`
);
```

The rest of the flow (sign `message`, `POST` it back with `wallet`/`message`/`signature`) is identical — `rise_id`/`impersonate` travel embedded in `message`, not as separate POST fields.

<Warning>
  Never expose `WALLET_PRIVATE_KEY`/`PRIVATE_KEY` in client-side code. Sign server-side, or use a dedicated [secondary wallet](/authentication/secondary-wallets) scoped to API access.
</Warning>

## Errors

| Status | Cause                                                                                                                                                                                                                                                                                                                                                                                          |
| ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | Missing/invalid `wallet`, `message`, `signature`, `rise_id`, or `impersonate`; wallet not verified for personal auth.                                                                                                                                                                                                                                                                          |
| `401`  | Missing or expired JWT on a protected endpoint.                                                                                                                                                                                                                                                                                                                                                |
| `403`  | Signature doesn't match the message; `wallet` isn't the RiseID's `Owner`/a delegate; `impersonate` doesn't have a manager role (`team_admin`/`team_finance_admin`/`team_delegate_admin`/`team_payment_initiator`) on `rise_id`; or the company isn't authorized for B2B API access (this last case uses a different [response envelope](/v1-legacy/overview#response-format) than the others). |

Still stuck? See [Troubleshooting](/v1-legacy/troubleshooting).

<Note>
  If your account hasn't finished migrating yet, these calls are transparently forwarded to the original V1 backend — no behavior change on your end.
</Note>
