# Get SIWE (V1 Legacy) Source: https://docs.riseworks.io/api-reference-v1/auth/get-siwe-v1-legacy openapi/openapi-v1.yaml GET /v1/auth/api/siwe V1-compatible endpoint to get SIWE message. Routes to V2 if user is migrated. # Verify SIWE (V1 Legacy) Source: https://docs.riseworks.io/api-reference-v1/auth/verify-siwe-v1-legacy openapi/openapi-v1.yaml POST /v1/auth/api/siwe V1-compatible endpoint to verify SIWE signature. Routes to V2 if user is migrated. # List invites (V1 Legacy) Source: https://docs.riseworks.io/api-reference-v1/invites/list-invites-v1-legacy openapi/openapi-v1.yaml GET /v1/invites V1-compatible endpoint to list pending invites for a company/team. # Send invites (V1 Legacy) Source: https://docs.riseworks.io/api-reference-v1/invites/send-invites-v1-legacy openapi/openapi-v1.yaml POST /v1/invites V1-compatible endpoint to send contractor/client invites to a company/team. # Send warmed invites (V1 Legacy) Source: https://docs.riseworks.io/api-reference-v1/invites/send-warmed-invites-v1-legacy openapi/openapi-v1.yaml POST /v1/invites/warmed V1-compatible endpoint to send pre-filled (KYC-primed) invites, in light or full mode. # Create batch payment intents (V1 Legacy) Source: https://docs.riseworks.io/api-reference-v1/payments/create-batch-payment-intents-v1-legacy openapi/openapi-v1.yaml POST /v1/payments/batch-pay/intents V1-compatible endpoint to create draft (unscheduled) batch payment intents. # Execute batch payment (V1 Legacy) Source: https://docs.riseworks.io/api-reference-v1/payments/execute-batch-payment-v1-legacy openapi/openapi-v1.yaml POST /v1/payments/batch-pay V1-compatible endpoint to broadcast a previously-signed batch of pay-now payments. # Execute instant payment (V1 Legacy) Source: https://docs.riseworks.io/api-reference-v1/payments/execute-instant-payment-v1-legacy openapi/openapi-v1.yaml POST /v1/payments/pay V1-compatible endpoint to broadcast a previously-signed single pay-now payment. # List payments (V1 Legacy) Source: https://docs.riseworks.io/api-reference-v1/payments/list-payments-v1-legacy openapi/openapi-v1.yaml GET /v1/payments V1-compatible endpoint to list/paginate payments for a company/team. # Prepare batch payment (V1 Legacy) Source: https://docs.riseworks.io/api-reference-v1/payments/prepare-batch-payment-v1-legacy openapi/openapi-v1.yaml PUT /v1/payments/batch-pay V1-compatible endpoint to prepare a batch of pay-now payments for signing. Returns EIP-712 typed data. # Prepare instant payment (V1 Legacy) Source: https://docs.riseworks.io/api-reference-v1/payments/prepare-instant-payment-v1-legacy openapi/openapi-v1.yaml PUT /v1/payments/pay V1-compatible endpoint to prepare a single pay-now payment for signing. Returns EIP-712 typed data. # Get RiseID balance (V1 Legacy) Source: https://docs.riseworks.io/api-reference-v1/riseid/get-riseid-balance-v1-legacy openapi/openapi-v1.yaml GET /v1/riseid/:rise_id/balance # Get team talent (V1 Legacy) Source: https://docs.riseworks.io/api-reference-v1/teams/get-team-talent-v1-legacy openapi/openapi-v1.yaml GET /v1/teams/:teamId/talent/:talentId # List team talent (V1 Legacy) Source: https://docs.riseworks.io/api-reference-v1/teams/list-team-talent-v1-legacy openapi/openapi-v1.yaml GET /v1/teams/:teamId/talent # List teams (V1 Legacy) Source: https://docs.riseworks.io/api-reference-v1/teams/list-teams-v1-legacy openapi/openapi-v1.yaml GET /v1/teams V1-compatible endpoint to list the authenticated user's teams. # Terminate team talent (V1 Legacy) Source: https://docs.riseworks.io/api-reference-v1/teams/terminate-team-talent-v1-legacy openapi/openapi-v1.yaml DELETE /v1/teams/:teamId/talent/:talentId # Get SIWE Source: https://docs.riseworks.io/api-reference/b2b-authentication/get-siwe /api-reference/openapi.yaml get /v2/auth/siwe Retrieves a SIWE message for authentication. Wallet must hold an allowed role on the user RiseID (Viewer recommended for B2B API auth). # Verify a SIWE message and return a JWT for API authentication Source: https://docs.riseworks.io/api-reference/b2b-authentication/verify-a-siwe-message-and-return-a-jwt-for-api-authentication /api-reference/openapi.yaml post /v2/auth/verify Verifies a signed SIWE message and returns a JWT. Viewer on personal RiseID authenticates only; creating payment intents requires Team RiseAccount PaymentInitiator (or Owner/Payer) via POST/PUT /v2/payments. # Get entity balance Source: https://docs.riseworks.io/api-reference/b2b-balance/get-entity-balance /api-reference/openapi.yaml get /v2/balance Retrieves balance and details of an entity using their nanoid (te- for team, co- for company, us- for user). # Create a single payment Source: https://docs.riseworks.io/api-reference/b2b-bill-pay/create-a-single-payment /api-reference/openapi.yaml put /v2/payments/external_entity/instant # Create an instant payment Source: https://docs.riseworks.io/api-reference/b2b-bill-pay/create-an-instant-payment /api-reference/openapi.yaml post /v2/payments/external_entity/instant # Create external recipient for billpay Source: https://docs.riseworks.io/api-reference/b2b-bill-pay/create-external-recipient-for-billpay /api-reference/openapi.yaml post /v2/payments/teams/{team_nanoid}/external_entity/ # Get external recipients for team Source: https://docs.riseworks.io/api-reference/b2b-bill-pay/get-external-recipients-for-team /api-reference/openapi.yaml get /v2/payments/teams/{team_nanoid}/external_entity # Create admin invites typed data Source: https://docs.riseworks.io/api-reference/b2b-invites/create-admin-invites-typed-data /api-reference/openapi.yaml post /v2/invites/manager Create admin invites for a company or team. This is used to invite users to the company or team. # Create invites for employees and contractors Source: https://docs.riseworks.io/api-reference/b2b-invites/create-invites-for-employees-and-contractors /api-reference/openapi.yaml post /v2/invites Create invites/warm invites for employees and contractors. This is used to invite users to team. # Execute admin invites Source: https://docs.riseworks.io/api-reference/b2b-invites/execute-admin-invites /api-reference/openapi.yaml put /v2/invites/manager Execute admin invites for a company or team. This is used to execute admin invites for a company or team. # Get Invites Source: https://docs.riseworks.io/api-reference/b2b-invites/get-invites /api-reference/openapi.yaml get /v2/invites List all pending invites for a company or team. User must be team_admin for teams or org_admin for companies. # Create typed data for payments Source: https://docs.riseworks.io/api-reference/b2b-payments/create-typed-data-for-payments /api-reference/openapi.yaml post /v2/payments Create typed data for payments. The typed data can be signed and then executed to create the payments on-chain. Intent creation (pay_now=false) requires Team RiseAccount PaymentInitiator (or Owner/Payer); Viewer on personal RiseID is for SIWE auth only. # Execute Payments Source: https://docs.riseworks.io/api-reference/b2b-payments/execute-payments /api-reference/openapi.yaml put /v2/payments Execute payments by signing the typed data and sending the signature along with the request. # Query Payments Source: https://docs.riseworks.io/api-reference/b2b-payments/query-payments /api-reference/openapi.yaml get /v2/payments Query payments for a team, optionally filtered by recipient and date range. # Create a team Source: https://docs.riseworks.io/api-reference/b2b-teams/create-a-team /api-reference/openapi.yaml post /v2/teams # Delete team member Source: https://docs.riseworks.io/api-reference/b2b-teams/delete-team-member /api-reference/openapi.yaml delete /v2/teams/{team_nanoid}/member/{user_nanoid} # Get team by nanoid Source: https://docs.riseworks.io/api-reference/b2b-teams/get-team-by-nanoid /api-reference/openapi.yaml get /v2/teams/{team_nanoid} # Get team member settings Source: https://docs.riseworks.io/api-reference/b2b-teams/get-team-member-settings /api-reference/openapi.yaml get /v2/teams/{team_nanoid}/member/{user_nanoid}/settings # Get team member summary Source: https://docs.riseworks.io/api-reference/b2b-teams/get-team-member-summary /api-reference/openapi.yaml get /v2/teams/{team_nanoid}/member/{user_nanoid}/summary # Get team settings Source: https://docs.riseworks.io/api-reference/b2b-teams/get-team-settings /api-reference/openapi.yaml get /v2/teams/{team_nanoid}/settings # Get team users Source: https://docs.riseworks.io/api-reference/b2b-teams/get-team-users /api-reference/openapi.yaml get /v2/teams/{team_nanoid}/users Retrieves a list of users associated with the specified team identified by its nanoid. This endpoint is useful for obtaining user details for a specific team, including their names, email addresses, onboarding status, and payment eligibility. Supports an optional `emails` filter to look up one or many users in a single request (repeated keys or a comma-separated list, up to 100), and optional offset pagination via page/offset/count query params; without them the full list is returned. Emails that match no team member are simply absent from the response — compare the returned `email` values against what you sent to detect them. # Update team by nanoid Source: https://docs.riseworks.io/api-reference/b2b-teams/update-team-by-nanoid /api-reference/openapi.yaml put /v2/teams/{team_nanoid} # Update team member settings Source: https://docs.riseworks.io/api-reference/b2b-teams/update-team-member-settings /api-reference/openapi.yaml put /v2/teams/{team_nanoid}/member/{user_nanoid}/settings # Update team settings Source: https://docs.riseworks.io/api-reference/b2b-teams/update-team-settings /api-reference/openapi.yaml put /v2/teams/{team_nanoid}/settings # Get user organization's Source: https://docs.riseworks.io/api-reference/b2b-users/get-user-organizations /api-reference/openapi.yaml get /v2/user/organizations Retrieves a list of organizations associated with the logged-in user. # Get user team's Source: https://docs.riseworks.io/api-reference/b2b-users/get-user-teams /api-reference/openapi.yaml get /v2/user/teams Retrieves a list of teams associated with the logged-in user. # Update user address details Source: https://docs.riseworks.io/api-reference/b2b-users/update-user-address-details /api-reference/openapi.yaml put /v2/user/address # Update user avatar Source: https://docs.riseworks.io/api-reference/b2b-users/update-user-avatar /api-reference/openapi.yaml post /v2/user/avatar/update # Get V1 Webhook Source: https://docs.riseworks.io/api-reference/b2b-webhooks-v1/get-v1-webhook /api-reference/openapi.yaml get /v1/webhooks # Delete Webhook Source: https://docs.riseworks.io/api-reference/b2b-webhooks/delete-webhook /api-reference/openapi.yaml delete /v2/webhooks/{webhook_nanoid} # Delivery History Source: https://docs.riseworks.io/api-reference/b2b-webhooks/delivery-history /api-reference/openapi.yaml get /v2/webhooks/{webhook_nanoid}/deliveries # Get Webhook Source: https://docs.riseworks.io/api-reference/b2b-webhooks/get-webhook /api-reference/openapi.yaml get /v2/webhooks/{webhook_nanoid} # Get Webhook Event Source: https://docs.riseworks.io/api-reference/b2b-webhooks/get-webhook-event /api-reference/openapi.yaml get /v2/webhooks/{webhook_nanoid}/events/{event_nanoid} Returns the stored event row and payload when this event has at least one delivery for the webhook endpoint. # List Webhooks Source: https://docs.riseworks.io/api-reference/b2b-webhooks/list-webhooks /api-reference/openapi.yaml get /v2/webhooks # Register Webhook Source: https://docs.riseworks.io/api-reference/b2b-webhooks/register-webhook /api-reference/openapi.yaml post /v2/webhooks/register # Retry Delivery Source: https://docs.riseworks.io/api-reference/b2b-webhooks/retry-delivery /api-reference/openapi.yaml post /v2/webhooks/retry/{delivery_nanoid} # Test Webhook Source: https://docs.riseworks.io/api-reference/b2b-webhooks/test-webhook /api-reference/openapi.yaml post /v2/webhooks/test/{webhook_nanoid} # Update Webhook (v1/v2 cannot be changed; register a new endpoint to switch version) Source: https://docs.riseworks.io/api-reference/b2b-webhooks/update-webhook-v1v2-cannot-be-changed;-register-a-new-endpoint-to-switch-version /api-reference/openapi.yaml put /v2/webhooks/{webhook_nanoid} # Getting API Access Source: https://docs.riseworks.io/authentication/api-access The two prerequisites for calling the Rise B2B API: your company enabled for the environment, and your wallet authorized on your RiseID. Before your first authenticated call works, two separate things have to be true. They fail in different ways, so it helps to know both up front: 1. **Rise has enabled your company for the B2B API** in the environment you're calling. This is a per-company switch that only Rise can flip, and staging and production are enabled separately. 2. **Your API wallet is authorized on your RiseID**, meaning it holds an Owner, Payer, or Treasurer role. You set this up yourself in the app. If either is missing you get a `403`, even when your credentials and code are correct. The two checks happen at different points in the flow: ```mermaid theme={null} flowchart TD A[SIWE handshake] --> C{Wallet holds an authorized
role on the RiseID?} C -- No --> C1["403 · Wallet is not an authorized member
→ Authorize your API wallet"] C -- Yes --> J[JWT issued] J --> B["First scoped call
(payments, teams, balances, ...)"] B --> G{Company enabled for the
B2B API in this environment?} G -- No --> G1["403 · B2B_ACCESS_DENIED
→ Ask Rise to enable your company"] G -- Yes --> D[Request succeeds] ``` The wallet-role check runs during the SIWE handshake, so a wrong or unauthorized wallet fails at login. Company enablement is checked later, on the first request that names a company or team, so an un-enabled company can still log in and read `/v2/me` but gets a `403` the moment it touches a team or payment. ## Enable your company for B2B API access The B2B API is off by default. Turning it on for your company is a support request: reach Rise support through your dedicated Slack channel if you have one, the in-app chat, or [hello@riseworks.io](mailto:hello@riseworks.io). Include: * Your company name and RiseID (or company nanoid, the `co-...` value). * The environment you want enabled: **staging**, **production**, or both. **Staging and production are enabled independently.** They run on separate databases, so enabling your company in staging does not enable it in production. Request each environment you plan to use. Most teams ask for staging first to build against, then production before going live. Until this is done, calls that resolve to your company return: ```json theme={null} { "success": false, "data": "Company is not enabled for B2B API access", "error_code": "B2B_ACCESS_DENIED" } ``` ## Finding your RiseID The SIWE handshake authenticates a **user**, so the `riseid` you send is your **personal (user) RiseID**, not a company or team RiseID and not your Rise Account address. Sending anything else returns `404 No entity found with riseid `. To find it in the app: Sign in to [app.riseworks.io](https://app.riseworks.io), open the user menu on the top bar (or the **Account** section of the left menu), and go to **My Profile**. Under **Details**, copy the value in the **Rise ID** field. It's a 42-character address that starts with `0x`, for example `0x2DF5...b089`. My Profile page: the left-menu My Profile item marked 1, an arrow to the Rise ID field marked 2, and the Rise Account field marked not this The same page shows a second address labelled **Rise Account**. That is not your RiseID and it won't authenticate. Use the field labelled **Rise ID**. Mixing the two up is a common cause of the `404` above. **Send the bare `0x` address, with no prefix.** The Rise ID card in the sidebar shows the same value with a network prefix (`arb4:0x…`). Drop the `arb4:` (or `eth:` / `arb1:`) part, the API matches on the plain `0x…` shown in the **Rise ID** field. It is also not one of your `us-`, `co-`, or `te-` nanoids. A prefixed value or a nanoid is the single most common cause of `404 No entity found with riseid`. Company and team RiseIDs are used for API operations like payments and balances, addressed by their `co-` / `te-` nanoids rather than by the SIWE handshake. See [RiseID](/concepts/riseid) for how the identity hierarchy fits together. ## Authorize your API wallet The wallet you sign with has to be authorized on your personal RiseID before the handshake will hand you a JWT. Authorizing it in the app grants it the Owner role, which is one of the roles the B2B API accepts (Owner, Payer, and Treasurer all work). Security page: the left-menu Security item marked 1 and an arrow to the Add Wallet button marked 2 From the user menu or the **Account** section of the left menu, go to **Security** ([app.riseworks.io/account/security](https://app.riseworks.io/account/security)). Under **Authorized Wallets**, click **Add Wallet**, choose the wallet type, paste the wallet's address, and confirm. Sign the prompt with a wallet that is already authorized on your RiseID. This writes the new wallet's role on-chain. Give the on-chain transaction time to confirm before you authenticate. Until it's mined, the SIWE handshake still sees the wallet as unauthorized and returns a `403`. The Add authorized wallet modal with Wallet Type and Wallet Address fields **This wallet's private key can move money. Guard it accordingly.** The role you just granted is signing authority, not just a login. Anyone who holds the key can authenticate as you and sign payments and withdrawals for every team the wallet can reach, and Owner (the role the app grants here) can do everything the account can, including authorizing more wallets. Keep the key in a secret manager or HSM, never in code, a repo, a log, or anything client-side. If it ever leaks, remove the wallet under **Security → Authorized Wallets** straight away, which strips its on-chain role, then switch to a fresh one. A leaked key keeps working until you revoke it, unlike a JWT that expires in 24 hours. Use a dedicated wallet for API operations rather than one holding funds. It needs no balance, since Rise pays the gas. See [Secondary Wallets](/authentication/secondary-wallets). Until the wallet is authorized and confirmed on-chain, the handshake returns: ``` Wallet 0x... is not an authorized member of RiseID 0x.... Grant it an Owner, Payer, or Treasurer role and wait for the on-chain transaction to confirm before retrying. ``` Adding a wallet under **Security → Authorized Wallets** authorizes it on your personal RiseID, and that authority carries down to the companies and teams you own. If you'd rather scope a wallet to one team, switch to that team's workspace, open **Settings**, and use **Delegate Wallets → Add delegate** (name plus address). The app text there reads "Add delegate wallets to authorize API transactions." Selecting a specific Payer or Treasurer role (rather than Owner) isn't exposed in the app yet. If you need a wallet scoped to one of those roles, ask Rise when you request access. ## Check you're set up Once both prerequisites are in place, run the [authentication handshake](/authentication/authentication) and confirm a couple of calls: * `GET /v2/me` returns your user. This proves the wallet is authorized and the JWT is valid. * A scoped call like fetching a team or balance succeeds. That confirms your company is enabled for the environment. If you're still blocked, match the response to the cause: | Response | Cause | Fix | | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | | `404 No entity found with riseid ` | The `riseid` has a network prefix (`arb4:`), is a `us-`/`co-`/`te-` nanoid, or is a company/team RiseID | Send the bare `0x` from the **Rise ID** field on My Profile, prefix stripped | | `400 Wallet address is required` / `400 riseid is required` | `GET /v2/auth/siwe` was called without the `wallet` or `riseid` query param | Pass both: `/v2/auth/siwe?wallet=<0x>&riseid=<0x>` | | `403 Wallet is not an authorized member...` | The signing wallet has no authorized role on the RiseID, or the grant hasn't confirmed on-chain yet | Authorize the wallet under Security, then wait for the transaction to confirm | | `403 Company is not enabled for B2B API access` | Company not enabled for this environment | Ask Rise to enable it, and check you're pointed at the environment you requested | | `401 Authentication required. Please provide Authorization...` | The JWT is missing or malformed on a call that needs it | Send `Authorization: Bearer ` from the handshake (the SDK does this for you) | | `SIWE error: Signature does not match address...` | A different wallet signed than the one in the message, or the message was altered before signing | Sign the exact `data.siwe` string with the same wallet you passed as `wallet` | The **Authorized Wallets** panel doesn't list wallets that already hold a role, so an empty panel doesn't mean the save failed. Confirm a wallet's role from the SIWE handshake succeeding, or by checking the RiseID on Arbiscan. ## Next steps Run the SIWE handshake and get your JWT Staging and production base URLs How users, companies, and teams relate Use a dedicated wallet for API operations # Authentication Source: https://docs.riseworks.io/authentication/authentication Learn how to authenticate with Rise using the SDK # Authentication **Two things have to be in place before your first authenticated call**, and the API returns a `403` even with correct code if either is missing: 1. Rise has to enable your company for B2B API access in the environment you're calling. Staging and production are enabled separately, so a request for one does not carry to the other. 2. The wallet you authenticate with has to hold an authorized role (Owner, Payer, or Treasurer) on your RiseID. Both are walked through in [Getting API Access](/authentication/api-access). Set them up there first, then come back here for the handshake itself. Rise implements a dual authentication system to ensure maximum security for your integrations. JWT tokens provide session management for all API operations, while SIWE (Sign-In with Ethereum) is required to obtain JWT tokens and adds blockchain-based verification for sensitive transactions. The Rise SDK simplifies this process by handling both authentication layers automatically. Use your wallet and private key for automatic authentication Use a pre-generated JWT token for direct API access ## Authentication Layers Rise's dual authentication system consists of two complementary layers: ### **JWT Authentication Layer** * **Session management**: Provides secure session management for all API operations * **User identity**: Verifies user identity and permissions * **Access control**: Handles API access control and rate limiting * **Token-based**: Uses JWT tokens for secure communication ### **SIWE Authentication Layer** * **JWT token generation**: Required to obtain JWT tokens for API access * **Blockchain verification**: Uses cryptographic wallet signing for authentication * **Enhanced security**: Provides additional security for sensitive operations * **Wallet-based**: Uses your wallet credentials for comprehensive access * **Immutable proof**: Creates immutable proof of user intent and authorization * **Write operations**: Required for the majority of write operations in Rise ## API Authentication Requirements Different API endpoints require different levels of authentication: ### **JWT Authentication (Read Operations)** * User profile information (`/me`) * Company information (`/companies`) * Team management (`/teams`) * Balance queries (`/entity-balance`) * Transaction history (`/transactions`) **Note**: JWT tokens are obtained through SIWE authentication ### **Dual Authentication (Sensitive Operations)** * Payment processing (`/payments`) * Withdrawals (`/withdrawals`) * Manager invites (`/invites`) * Company settings updates * High-value transactions **Note**: These operations require both JWT (obtained via SIWE) and additional SIWE signing **SDK Recommendation**: Use the Rise SDK for automatic authentication handling. The SDK will use SIWE to generate JWT tokens and automatically add SIWE signing when required for sensitive operations. ## Authentication Flows Whether you use the SDK or call the API directly, the same handshake runs underneath: ask Rise for a SIWE message, sign it with your wallet, then exchange the signature for a JWT. The SDK does all three steps for you. ```mermaid theme={null} sequenceDiagram participant App as Your integration participant Wallet as API wallet participant Rise as Rise API App->>Rise: GET /v2/auth/siwe (wallet, riseid) Note over Rise: Checks the wallet's on-chain role on the RiseID Rise-->>App: 200 with data.siwe (the message to sign) App->>Wallet: Sign the SIWE message Wallet-->>App: signature App->>Rise: POST /v2/auth/verify (message, sig, nonce) Rise-->>App: 200 with data.jwt App->>Rise: Any endpoint with Authorization: Bearer JWT Rise-->>App: 200 ``` The JWT is valid for 24 hours. The SDK refreshes it for you; if you drive the handshake yourself, re-run it when the token expires. ### SIWE Authentication Flow The SDK manages the complete SIWE authentication process: Configure the SDK with your Rise ID and private key SDK generates SIWE message, signs it, and obtains JWT token SDK automatically handles JWT token renewal Execute API calls with automatic authentication handling ### JWT Authentication Flow For integrations using pre-generated JWT tokens: Retrieve JWT token from your existing authentication system Configure the SDK with your JWT token Execute API calls with automatic JWT header inclusion Handle token expiration through manual renewal or token refresh ## Understanding Authentication ### How SIWE Authentication Works ```javascript theme={null} const { ethers } = require('ethers'); // Production: https://integrations-api.riseworks.io // Staging: https://integrations-api.staging-riseworks.io const BASE_URL = 'https://integrations-api.riseworks.io'; async function understandSIWEAuthentication() { try { const wallet = new ethers.Wallet(process.env.WALLET_PRIVATE_KEY); // Step 1: Ask Rise for a SIWE message. The wallet address and your // personal (user) RiseID both go in the query string. const siweResponse = await fetch( `${BASE_URL}/v2/auth/siwe?wallet=${wallet.address}&riseid=${process.env.RISE_ID}` ); if (!siweResponse.ok) { const { data } = await siweResponse.json(); throw new Error(`SIWE request failed (${siweResponse.status}): ${data}`); } const siweData = await siweResponse.json(); const message = siweData.data.siwe; // Step 2: Sign the message with your private key const signature = await wallet.signMessage(message); // Step 3: Read the nonce back out of the signed message const nonce = message.match(/Nonce: (.+)/)?.[1]?.trim(); // Step 4: Exchange the signature for a JWT const verifyResponse = await fetch(`${BASE_URL}/v2/auth/verify`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message, sig: signature, nonce }) }); const jwtData = await verifyResponse.json(); const jwtToken = jwtData.data.jwt; console.log('JWT token generated:', jwtToken); return jwtToken; } catch (error) { console.error('SIWE authentication failed:', error.message); throw error; } } // This shows how SIWE authentication works internally // The Rise SDK handles this automatically for you const jwtToken = await understandSIWEAuthentication(); ``` ### How JWT Token Authentication Works ```javascript theme={null} async function understandJWTUsage(jwtToken) { try { // Use JWT token for API calls const response = await fetch('https://integrations-api.riseworks.io/v2/me', { method: 'GET', headers: { 'Authorization': `Bearer ${jwtToken}`, 'Content-Type': 'application/json' } }); const userData = await response.json(); console.log('User data:', userData); return userData; } catch (error) { console.error('API call failed:', error.message); throw error; } } // This shows how JWT authentication works internally // The Rise SDK handles this automatically for you const userData = await understandJWTUsage(jwtToken); ``` ## SDK Configuration Options The Rise SDK provides two configuration options to access the APIs: ### **Using Rise ID and Private Key** * **Automatic JWT generation**: SDK handles SIWE authentication automatically * **Comprehensive access**: Full access to all API operations * **Session management**: Automatic JWT token renewal * **Wallet-based**: Uses your wallet credentials for authentication ### **Using JWT Token Only** * **Pre-generated tokens**: Use existing JWT tokens for authentication * **Basic access**: Access to read operations and basic integrations * **Simple setup**: Direct API access without wallet signing * **Limited scope**: Cannot perform sensitive operations requiring SIWE ## Quick Start with SDK ### Using Rise ID Authentication (Recommended) ```javascript theme={null} const { RiseApiClient } = require('@riseworks/sdk'); // Initialize with Rise ID and private key for comprehensive API access const client = new RiseApiClient({ riseIdAuth: { riseId: process.env.RISE_ID, privateKey: process.env.WALLET_PRIVATE_KEY } }); // SDK automatically handles both JWT and SIWE authentication const user = await client.me.get(); console.log('Authenticated as:', user.data.name); ``` ### Using JWT Token Authentication ```javascript theme={null} const { RiseApiClient } = require('@riseworks/sdk'); // Note: You need to obtain the JWT token manually through SIWE authentication // or from your existing authentication system before using this approach // Initialize with JWT token for basic API access // To get JWT: Use SIWE authentication first, then extract the JWT token const client = new RiseApiClient({ jwtToken: process.env.JWT_TOKEN }); // Ready for read-only operations const user = await client.me.get(); console.log('Authenticated as:', user.data.name); ``` ```javascript theme={null} const { ethers } = require('ethers'); const BASE_URL = 'https://integrations-api.riseworks.io'; async function generateJwtManually() { try { const wallet = new ethers.Wallet(process.env.WALLET_PRIVATE_KEY); // Step 1: Get a SIWE message const siweResponse = await fetch( `${BASE_URL}/v2/auth/siwe?wallet=${wallet.address}&riseid=${process.env.RISE_ID}` ); const siweData = await siweResponse.json(); const message = siweData.data.siwe; // Step 2: Sign it, then read the nonce back out of the message const signature = await wallet.signMessage(message); const nonce = message.match(/Nonce: (.+)/)?.[1]?.trim(); // Step 3: Verify the signature and get the JWT const verifyResponse = await fetch(`${BASE_URL}/v2/auth/verify`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message, sig: signature, nonce }) }); const jwtData = await verifyResponse.json(); const jwtToken = jwtData.data.jwt; console.log('Manually generated JWT token:', jwtToken); return jwtToken; } catch (error) { console.error('JWT generation failed:', error.message); throw error; } } // Usage const jwtToken = await generateJwtManually(); ``` ### JWT Token Management ```javascript theme={null} // Store JWT token securely const jwtToken = await client.getJwtToken(); // Use JWT token for subsequent API calls const jwtClient = new RiseApiClient({ jwtToken: jwtToken }); // Verify JWT token validity const isValid = await client.isJwtValid(); console.log('JWT token valid:', isValid); // Refresh JWT token when needed const newToken = await client.refreshJwtToken(); console.log('New JWT token:', newToken); ``` **SDK Recommendation**: Use the SDK's automatic JWT generation for simplicity. The SDK handles all the complexity of SIWE message generation, signing, and JWT token management for you. ## SDK Features ### Automatic Token Management ```javascript theme={null} // SDK automatically handles JWT token renewal const client = new RiseApiClient({ riseIdAuth: { riseId: process.env.RISE_ID, privateKey: process.env.WALLET_PRIVATE_KEY } }); // SDK automatically generates new JWT tokens when needed const user = await client.me.get(); // No manual token management required ``` ### Token Generation ```javascript theme={null} // Manually generate JWT token when necessary const newToken = await client.generateJwtToken(); console.log('New JWT token:', newToken); ``` ### Manual Token Refresh ```javascript theme={null} // Manually refresh JWT token when necessary const newToken = await client.refreshJwtToken(); console.log('New JWT token:', newToken); ``` ### Token Validation ```javascript theme={null} // Verify current JWT token validity const isValid = await client.isJwtValid(); console.log('JWT valid:', isValid); ``` ## Error Handling The SDK provides comprehensive error handling for authentication scenarios: ```javascript theme={null} const { RiseApiClient } = require('@riseworks/sdk'); async function handleAuthenticationErrors() { try { const client = new RiseApiClient({ riseIdAuth: { riseId: process.env.RISE_ID, privateKey: process.env.WALLET_PRIVATE_KEY } }); const user = await client.me.get(); console.log('Success:', user.data); } catch (error) { if (error.message.includes('Rise ID and private key are required')) { console.error('Missing credentials - verify your environment variables'); } else if (error.message.includes('Invalid Rise ID address')) { console.error('Invalid Rise ID format - should be 0x + 40 hex characters'); } else if (error.message.includes('Invalid private key')) { console.error('Invalid private key format - should be 0x + 64 hex characters'); } else if (error.message.includes('401')) { console.error('Authentication failed - verify your credentials'); } else if (error.message.includes('403')) { console.error('Permission denied - verify your role permissions'); } else { console.error('Unexpected error:', error.message); } } } ``` ## Common Error Scenarios | Response | What it means | How to fix | | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `404` `No entity found with riseid ` | The `riseid` isn't a user RiseID: it carries a network prefix (`arb4:`), is a `us-`/`co-`/`te-` nanoid, or is a company/team RiseID. | Send the bare `0x` user RiseID with the prefix stripped. See [Finding your RiseID](/authentication/api-access#finding-your-riseid). | | `400` `Wallet address is required` / `riseid is required` | `GET /v2/auth/siwe` was called without the `wallet` or `riseid` query param. | Pass both: `/v2/auth/siwe?wallet=<0x>&riseid=<0x>`. | | `403` `Wallet is not an authorized member of RiseID ...` | The wallet you're signing with holds no Owner, Payer, or Treasurer role on that RiseID. | Authorize the wallet on your RiseID, wait for the on-chain transaction to confirm, then retry. See [Authorize your API wallet](/authentication/api-access#authorize-your-api-wallet). | | `403` `Company is not enabled for B2B API access` | Your company isn't enabled for the B2B API in this environment. | Ask Rise to enable it for the environment you're calling. Staging and production are separate. See [Enable your company](/authentication/api-access#enable-your-company-for-b2b-api-access). | | `401 Unauthorized` | The JWT is missing, expired, or invalid. | Re-run the SIWE handshake for a fresh token. The SDK does this for you. | | `Invalid Rise ID address` | Rise ID isn't in the right format. | A Rise ID starts with `0x` and is 42 characters. | | `Invalid private key` | Private key isn't in the right format. | A private key starts with `0x` and is 66 characters. | ## Security Best Practices ### Recommended Practices: * **Use environment variables** for all sensitive data * **Use secondary wallets** for API operations * **Implement comprehensive error handling** ### Security Considerations: * **Never store private keys in code** * **Never commit credentials to version control** * **Avoid using main wallets** with significant funds for API operations * **Never ignore authentication errors** ## Next Steps Learn how to get and use private keys Use dedicated wallets for API operations See more SDK usage examples Comprehensive security guidelines **Ready to integrate?** Review our [Quickstart Guide](/quickstart) to begin using the Rise SDK with authentication. # Understanding Private Keys Source: https://docs.riseworks.io/authentication/private-keys Learn about private keys, their importance, and how to use them securely with Rise B2B API Understanding private keys is crucial for secure authentication and transaction signing in blockchain-based applications. ## What is a Private Key? A private key is a cryptographic secret that allows you to: * **Sign messages** to prove your identity * **Authorize transactions** on the blockchain * **Control digital assets** associated with your wallet * **Authenticate** with blockchain-based services Think of it as a digital signature that only you can create, proving you are who you claim to be. ## Why Private Keys Are Important Private keys are essential for secure blockchain interactions and API authentication: ### **Digital Identity Verification** * **Cryptographic proof**: Your private key creates mathematical proof of your identity * **Impossible to forge**: Only you can create signatures with your private key * **Immutable verification**: Signatures cannot be altered or tampered with * **Trustless authentication**: No need to trust third parties for identity verification ### **Transaction Authorization** * **Secure signing**: Private keys sign transactions to authorize them * **Intent verification**: Signatures prove you intended to perform the action * **Non-repudiation**: You cannot deny performing actions you've signed * **Blockchain security**: Ensures only authorized users can perform operations ### **API Authentication with Rise** * **SIWE signing**: Sign-In with Ethereum uses your private key for authentication * **Message signing**: Sign cryptographic messages to prove wallet ownership * **Session management**: Generate JWT tokens through secure signing * **Access control**: Control which operations your application can perform ## How Private Keys Are Used for Signing ### **Message Signing Process** When you authenticate with Rise, your private key is used to sign messages: Rise creates a cryptographic message with your Rise ID and timestamp Your private key creates a unique signature for this message Rise verifies the signature using your wallet's public address If valid, Rise grants you access and provides a JWT token ### **Security Benefits of Signing** * Proves you control the wallet * Verifies your Rise account ownership * Prevents impersonation attacks * Ensures account security * Authorizes specific operations * Prevents unauthorized transactions * Creates audit trails * Ensures transaction integrity * Generates secure JWT tokens * Manages API session security * Handles token renewal * Maintains secure connections * Controls API permissions * Limits operation scope * Prevents privilege escalation * Ensures proper authorization ## Creating Your First Wallet If you don't have a wallet yet, you'll need to create one first. Here are the most popular options: ### Option 1: MetaMask (Browser Extension) Go to [metamask.io](https://metamask.io) and install the browser extension Click "Create a Wallet" and follow the setup process Write down your 12-word recovery phrase and store it securely Create a strong password for your MetaMask wallet Complete the recovery phrase verification ### Option 2: Coinbase Wallet (Browser Extension) Go to [wallet.coinbase.com](https://wallet.coinbase.com) and install the browser extension Click "Create new wallet" and follow the setup process Write down your 12-word recovery phrase and store it securely Create a strong password for your Coinbase Wallet Complete the recovery phrase verification ### Option 3: Coinbase Wallet (Mobile App) Install Coinbase Wallet from your app store Open the app and tap "Create new wallet" Write down your 12-word recovery phrase Complete the recovery phrase verification Enable biometric authentication if available **Important**: When creating a wallet, always write down your recovery phrase (seed phrase) and store it securely. This is the only way to recover your wallet if you lose access to your device. ## How to Get Your Private Key ### From MetaMask Click on the MetaMask extension in your browser Click on the three dots menu → Account details Click "Export Private Key" and enter your password Copy the private key (starts with 0x) ### From Other Wallets Most wallets provide an export function: * **Trust Wallet**: Settings → Security → Export Private Key * **Coinbase Wallet**: Settings → Advanced → Export Private Key * **Rainbow**: Settings → Security → Export Private Key ### Generate New Key ```typescript theme={null} import { ethers } from 'ethers'; // Generate a new wallet const wallet = ethers.Wallet.createRandom(); // Get the private key const privateKey = wallet.privateKey; const address = wallet.address; console.log('Private Key:', privateKey); console.log('Address:', address); ``` ## Private Key Security Best Practices * Use password managers * Hardware security modules (HSM) * Encrypted storage * Never store in plain text * Limit access to authorized personnel * Use role-based access * Implement audit logging * Regular access reviews * Create secure backups * Use multiple locations * Test recovery process * Update backups regularly * Monitor wallet activity * Set up alerts * Regular security audits * Track usage patterns ## Private Key Format Private keys in Ethereum are: * **64 characters** long (32 bytes) * **Hexadecimal** format * **Start with 0x** * **Case sensitive** Example: `0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef` ## Why Private Keys Matter Private keys are the foundation of blockchain-based security and provide several critical benefits: * Mathematically unbreakable * Quantum-resistant algorithms * Zero-knowledge proofs * Tamper-evident signatures * You control your own identity * No reliance on third-party authentication * Direct ownership of digital assets * Self-sovereign identity * Blockchain-verifiable signatures * Immutable transaction history * Transparent audit logs * Compliance-ready records * Self-sovereign identity * Regulatory compliance * Audit trail requirements * Data privacy standards # Secondary Wallets for API Operations Source: https://docs.riseworks.io/authentication/secondary-wallets Learn how to use dedicated secondary wallets for secure API operations with Rise B2B API For enhanced security, we strongly recommend using a dedicated secondary wallet for API operations. **Important**: Your secondary wallet does not need any funds - the private key is only used for cryptographic signing, not for transactions. Rise handles all gas fees and transaction costs. **No funds in the wallet does not mean low stakes.** The private key is signing authority over your Rise account, so whoever holds it can authenticate as you and pay or withdraw from any team the wallet's role can reach. That's the whole reason to isolate it. Keep it in a secret manager or HSM, never in code or version control, and if it ever leaks, revoke the wallet under **Security → Authorized Wallets** before anything else, which strips its on-chain role. A leaked key keeps working until you revoke it, unlike a JWT that expires in 24 hours. ## Why Use a Secondary Wallet? Using a dedicated secondary wallet for API operations provides several critical security benefits: * Keeps your primary wallet secure * Limits potential exposure * Reduces risk of fund loss * Isolates API activities * Minimal funds at risk * Easy to monitor activity * Quick to replace if compromised * Clear audit trail * Dedicated for API operations * Separate from personal transactions * Easy to manage permissions * Simplified monitoring * Clear separation of concerns * Easier compliance reporting * Simplified audit trails * Better regulatory compliance ## Setting Up a Secondary Wallet Generate a new wallet specifically for API operations Store the private key securely (password manager, HSM) Test with staging environment first Set up alerts and regular monitoring ## Setting Up Your Secondary Wallet Once you have created a wallet using any of the methods described in [Understanding Private Keys](/authentication/private-keys), you can set it up as a secondary wallet for API operations: Create a new account in your existing wallet and name it "API Operations" Export the private key following your wallet's specific process Store the private key in a password manager or HSM No funds needed - Rise handles all gas fees **Refer to [Understanding Private Keys](/authentication/private-keys) for detailed wallet creation and private key extraction steps for MetaMask, Coinbase Wallet, and other wallet types.** ## Authorize the wallet on your RiseID Creating the wallet isn't enough on its own. Until you authorize it on your RiseID, the SIWE handshake rejects it with a `403`, because the wallet holds no role on your account yet. Add it under **Security → Authorized Wallets** in the app and wait for the on-chain grant to confirm. The full steps are in [Getting API Access](/authentication/api-access#authorize-your-api-wallet). ## Recovery Procedures ### If Wallet is Compromised Stop all API operations immediately Review recent transactions Generate replacement wallet Update environment variables Test with new wallet # Entity Balance Source: https://docs.riseworks.io/concepts/entity-balance Understanding entity balance management in the Rise ecosystem Entity balance management in Rise tracks funds on the blockchain for users, teams, and companies. Every entity has an associated balance that can be queried using their unique identifier. ## Entity Balance API ### Get Entity Balance Retrieve the current balance for a specific entity: ```bash theme={null} curl -X GET "${this.baseUrl}/v2/balance?nanoid=te-abc123def456" \ -H "Authorization: Bearer YOUR_JWT_TOKEN" ``` ```json theme={null} { "success": true, "data": { "rise_account_address": "0x1234567890abcdef...", "balances": [ { "currency": "USD", "balance_cents": 100000 }, { "currency": "EUR", "balance_cents": 50000 } ] } } ``` ### Supported Currencies You can specify which currencies to query: ```bash theme={null} curl -X GET "${this.baseUrl}/v2/balance?nanoid=te-abc123def456¤cies=USD,EUR" \ -H "Authorization: Bearer YOUR_JWT_TOKEN" ``` ## Balance Components Funds that can be used for payments and transactions Multiple currencies supported (USD, EUR, etc.) All amounts returned in cents for precision Balances updated in real-time from blockchain ## Authorization Requirements Different entity types have different authorization requirements: ### Team Balance Access * **Team Admin**: Full access to team balance * **Team Finance Admin**: Full access to team balance * **Team Viewer**: Read-only access to team balance * **Team Employee**: No access to team balance ### Company Balance Access * **Company Owner**: Full access to company balance * **Org Admin**: Full access to company balance * **Org Finance Admin**: Full access to company balance * **Org Viewer**: Read-only access to company balance ### User Balance Access * **User**: Can only access their own balance * **Others**: No access to user balance ## Integration Example ```javascript theme={null} class RiseEntityBalance { constructor(baseUrl, jwtToken) { this.baseUrl = baseUrl; this.headers = { 'Authorization': `Bearer ${jwtToken}`, 'Content-Type': 'application/json' }; } async getBalance(nanoid, currencies = null) { const params = new URLSearchParams({ nanoid }); if (currencies) { params.append('currencies', currencies.join(',')); } const response = await fetch( `${this.baseUrl}/v2/balance?${params}`, { headers: this.headers } ); return response.json(); } } // Usage example const balanceApi = new RiseEntityBalance( 'https://b2b-api.riseworks.io', // or your environment URL 'your-jwt-token' ); ``` ## Error Handling Common errors when working with entity balances: | HTTP Status | Description | Solution | | ----------- | ------------------------------------ | -------------------------------------- | | `400` | Bad Request - Invalid nanoid format | Ensure nanoid follows correct format | | `401` | Unauthorized - Invalid JWT token | Re-authenticate to get a valid JWT | | `403` | Forbidden - Insufficient permissions | Check user permissions for the entity | | `404` | Not Found - Entity doesn't exist | Verify the nanoid exists in the system | | `500` | Internal Server Error | Retry the request or contact support | ### Common Error Scenarios **Invalid Nanoid Format (400):** ```json theme={null} { "success": false, "data": "Invalid nanoid format: must start with te-, co-, or us-" } ``` **Entity Not Found (404):** ```json theme={null} { "success": false, "data": "No entity found with nanoid te-invalid123" } ``` **Permission Denied (403):** ```json theme={null} { "success": false, "data": "User us-abc123def456 must have admin access to team te-xyz789abc123. Current role: team_employee" } ``` ## Security Considerations **Always verify entity ownership and permissions** before accessing balance information. Only authorized users should have access to entity balances. * **Permission Validation**: Ensure users have proper permissions to access entity balances * **Rate Limiting**: Implement appropriate rate limiting for balance queries * **Audit Logging**: Log all balance access for security auditing ## Related Concepts Understanding nanoid identifiers Team management concepts Payment processing and flows Understanding access control **Balance Precision**: All balance amounts are returned in cents to maintain precision. Convert to decimal format only when displaying to users. # Entity Nanoid Source: https://docs.riseworks.io/concepts/entity-nanoid Understanding nanoid identifiers and their usage in the Rise ecosystem Entity Nanoid is the primary identifier system used throughout the Rise B2B API for all entities and operations. Unlike RiseID (used for authentication), nanoids are used for API operations, database references, and internal entity management. ## What is Nanoid? A nanoid is a unique, URL-friendly identifier that follows a specific format: **prefix + hyphen + 12-character random string**. All nanoids in Rise are exactly **15 characters long** and use a standardized prefix system to identify the entity type. ### Nanoid Format ``` [prefix]-[12 random characters] ``` **Example**: `te-abc123def456` (15 characters total) ## Core Entity Nanoids The primary entity types in Rise each have their own nanoid format: Format: `te-abc123def456`
Example: `te-xyz789abc123`
15 characters
Format: `co-abc123def456`
Example: `co-def456ghi789`
15 characters
Format: `us-abc123def456`
Example: `us-ghi789jkl012`
15 characters
## Complete Nanoid Reference Here are all the nanoid types used in Rise B2B API: ### Core Entities | Entity Type | Prefix | Format | Example | Description | | ----------- | ------ | ----------------- | ----------------- | ------------------ | | **Team** | `te` | `te-abc123def456` | `te-xyz789abc123` | Team or department | | **Company** | `co` | `co-abc123def456` | `co-def456ghi789` | Organization | | **User** | `us` | `us-abc123def456` | `us-ghi789jkl012` | Individual user | ### Financial & Operational Entities | Entity Type | Prefix | Format | Example | Description | | ----------------- | ------ | ----------------- | ----------------- | ------------------- | | **Payment** | `pa` | `pa-abc123def456` | `pa-pay123def456` | Individual payment | | **Payment Group** | `pg` | `pg-abc123def456` | `pg-grp123def456` | Batch payment group | | **Invite** | `in` | `in-abc123def456` | `in-inv123def456` | User invitation | ## Nanoid Usage in API ### Entity Balance API The most common usage is in the entity balance endpoint: ```bash theme={null} curl -X GET "https://api.rise.works/v2/balance?nanoid=te-abc123def456" \ -H "Authorization: Bearer YOUR_JWT_TOKEN" ``` ### Team Management API Team operations use team nanoids: ```bash theme={null} curl -X GET "https://api.rise.works/v2/teams/te-abc123def456/users" \ -H "Authorization: Bearer YOUR_JWT_TOKEN" ``` ### Payments API Payment operations use payment nanoids: ```bash theme={null} curl -X GET "https://api.rise.works/v2/payments?team_nanoid=te-abc123def456" \ -H "Authorization: Bearer YOUR_JWT_TOKEN" ``` ### Invites API Invite management uses invite nanoids: ```bash theme={null} curl -X GET "https://api.rise.works/v2/invites?nanoid=te-abc123def456" \ -H "Authorization: Bearer YOUR_JWT_TOKEN" ``` ## Nanoid Validation When working with nanoids, you should validate their format before making API calls: ### Validation Rules 1. **Length**: Must be exactly 15 characters 2. **Format**: Must follow the pattern `[prefix]-[12 random characters]` 3. **Prefix**: Must start with a valid prefix (`te-`, `co-`, `us-`, `pa-`, `pg-`, `in-`) ### Client-Side Validation ```javascript theme={null} class NanoidValidator { // Validate nanoid format static validateNanoid(nanoid) { if (!nanoid || typeof nanoid !== 'string') { return false; } // Must be exactly 15 characters if (nanoid.length !== 15) { return false; } // Must have a valid prefix const validPrefixes = ['te-', 'co-', 'us-', 'pa-', 'pg-', 'in-']; return validPrefixes.some(prefix => nanoid.startsWith(prefix)); } // Extract entity type from nanoid static getEntityType(nanoid) { if (nanoid.startsWith('te-')) return 'team'; if (nanoid.startsWith('co-')) return 'company'; if (nanoid.startsWith('us-')) return 'user'; if (nanoid.startsWith('pa-')) return 'payment'; if (nanoid.startsWith('pg-')) return 'payment_group'; if (nanoid.startsWith('in-')) return 'invite'; return 'unknown'; } // Generate API URL based on nanoid type static getApiUrl(nanoid, endpoint) { const entityType = this.getEntityType(nanoid); switch (entityType) { case 'team': return `/v2/teams/${nanoid}${endpoint}`; case 'company': return `/v2/companies/${nanoid}${endpoint}`; case 'user': return `/v2/users/${nanoid}${endpoint}`; case 'payment': return `/v2/payments/${nanoid}${endpoint}`; case 'invite': return `/v2/invites/${nanoid}${endpoint}`; default: throw new Error(`Unsupported entity type: ${entityType}`); } } } ``` ```typescript theme={null} interface NanoidInfo { entityType: string; prefix: string; randomPart: string; isValid: boolean; } class NanoidValidator { // Parse nanoid into components static parseNanoid(nanoid: string): NanoidInfo { if (!nanoid || typeof nanoid !== 'string') { return { entityType: 'unknown', prefix: '', randomPart: '', isValid: false }; } const isValid = this.validateNanoid(nanoid); const prefix = nanoid.substring(0, 3); // Includes the hyphen const randomPart = nanoid.substring(3); const entityType = this.getEntityType(nanoid); return { entityType, prefix, randomPart, isValid }; } // Validate nanoid format static validateNanoid(nanoid: string): boolean { if (!nanoid || typeof nanoid !== 'string') { return false; } // Must be exactly 15 characters if (nanoid.length !== 15) { return false; } // Must have a valid prefix const validPrefixes = ['te-', 'co-', 'us-', 'pa-', 'pg-', 'in-']; return validPrefixes.some(prefix => nanoid.startsWith(prefix)); } // Extract entity type from nanoid static getEntityType(nanoid: string): string { if (nanoid.startsWith('te-')) return 'team'; if (nanoid.startsWith('co-')) return 'company'; if (nanoid.startsWith('us-')) return 'user'; if (nanoid.startsWith('pa-')) return 'payment'; if (nanoid.startsWith('pg-')) return 'payment_group'; if (nanoid.startsWith('in-')) return 'invite'; return 'unknown'; } } ``` ## Error Handling Common errors when working with nanoids: | Error | Description | Solution | | ----------------------- | --------------------------------------- | ------------------------------------------------ | | `Invalid nanoid format` | Nanoid doesn't match expected pattern | Ensure nanoid is 15 characters with valid prefix | | `Entity not found` | Nanoid doesn't exist in system | Verify the nanoid exists and is correct | | `Permission denied` | User lacks access to entity | Check user permissions for the entity | | `Invalid entity type` | Nanoid type doesn't match expected type | Verify you're using the correct nanoid type | ### Common Error Scenarios **Invalid Nanoid Format:** ```json theme={null} { "success": false, "data": "Invalid nanoid format: must start with te-, co-, or us-" } ``` **Entity Not Found:** ```json theme={null} { "success": false, "data": "No entity found with nanoid te-invalid123" } ``` **Permission Denied:** ```json theme={null} { "success": false, "data": "User us-abc123def456 must have admin access to team te-xyz789abc123" } ``` ## Best Practices Always validate nanoid format before using Document which nanoid types your integration uses Never expose nanoid generation logic to clients Cache nanoid lookups for performance ### Security Considerations * **Client-side validation**: Always validate nanoid format before making API calls * **No sequential patterns**: Nanoids are cryptographically random * **Prefix validation**: Always validate the prefix matches expected entity type * **Length validation**: Ensure nanoids are exactly 15 characters ### Performance Tips * **Batch operations**: Use batch endpoints when working with multiple nanoids * **Caching**: Cache frequently accessed nanoid lookups * **Validation**: Validate nanoid format early to avoid unnecessary API calls ## Related Concepts Understanding RiseID for authentication Balance management with nanoid Team management concepts Understanding access control **Nanoid vs RiseID**: Remember that nanoids are used for API operations and database references, while RiseID is used for authentication and smart contract interactions. # Environments Source: https://docs.riseworks.io/concepts/environments Environment configurations for Rise B2B API staging and production # Environments Rise B2B API provides two environments for your integration needs: **staging** for testing and **production** for live operations. Use for testing and development Use for live operations ## SDK Environment Configuration The Rise SDK uses the `environment` field to determine which API endpoints to use. By default, it uses production, but you can change it to staging for testing. ### Environment Field Options ```javascript theme={null} const { RiseApiClient } = require('@riseworks/sdk'); // Production (default) - no environment field needed const prodClient = new RiseApiClient({ riseIdAuth: { riseId: process.env.RISE_ID, privateKey: process.env.WALLET_PRIVATE_KEY } }); // Staging - explicitly set environment to 'stg' const stagingClient = new RiseApiClient({ environment: 'stg', // This changes the API base URL to staging riseIdAuth: { riseId: process.env.RISE_ID, privateKey: process.env.WALLET_PRIVATE_KEY } }); ``` ### Environment Field Values | Value | API Base URL | Use Case | | ------------------ | ----------------------------------------------- | ----------------------- | | `'prod'` (default) | `https://integrations-api.riseworks.io` | Live operations | | `'stg'` | `https://integrations-api.staging-riseworks.io` | Testing and development | | `undefined` | `https://integrations-api.riseworks.io` | Same as 'prod' | ### Changing Environment at Runtime You can also change the environment after creating the client: ```javascript theme={null} const client = new RiseApiClient({ environment: 'stg', // Start with staging riseIdAuth: { riseId: process.env.RISE_ID, privateKey: process.env.WALLET_PRIVATE_KEY } }); // Later, switch to production client.updateEnvironment('prod'); // Check current environment console.log('Current environment:', client.getEnvironment()); // 'prod' ``` **Important**: Never use production credentials in staging environment, and never use staging credentials in production. Each environment has separate data and should be treated independently. B2B API access is enabled per environment. Being enabled in staging does not enable you in production, so request each one you need. See [Getting API Access](/authentication/api-access#enable-your-company-for-b2b-api-access). ## Support Troubleshooting environment issues Get help with environment setup **Need help with environment setup?** Contact our support team if you have questions about configuring your integration for staging or production environments. # Payments Source: https://docs.riseworks.io/concepts/payments Understand payment flows and security in Rise B2B API Rise B2B API supports secure, on-chain payments for payroll and business operations. All payments are processed through blockchain transactions with EIP-712 typed data signing for enhanced security. ## Payment Types Rise B2B supports two main payment types: Single payments to one recipient Multiple payments to multiple recipients in one transaction ## Payment Flow The Rise payment process follows a secure 3-step flow: Create payment and get typed data using POST /v2/payments Sign the typed data using your Web3 wallet Submit the signed data using PUT /v2/payments to execute on-chain ## Payment Security All payments are verified and executed on the blockchain EIP-712 signatures prevent tampering and replay attacks All API communications use encrypted HTTPS Verify contract addresses and chain IDs ## API Endpoints ### 1. Create Payment **POST** `${this.baseUrl}/v2/payments` Creates a payment and returns EIP-712 typed data for signing. ```bash theme={null} curl -X POST "${this.baseUrl}/v2/payments" \ -H "Authorization: Bearer YOUR_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "from": "te-abc123def456", "to": [ { "to": "us-xyz789abc123", "amount_cents": 100000, "currency_symbol": "USD", "invoice_description": "Salary payment for January 2024" } ], "pay_now": true, "network": "arbitrum" }' ``` ```json theme={null} { "success": true, "data": { "domain": { "name": "RiseAccountForwarder", "version": "1.0.0", "chainId": 42161, "verifyingContract": "0x..." }, "types": { "CreatePaymentsForwardRequest": [...] }, "typed_data": { "from": "0x...", "to": "0x...", "salt": "12345", "expires": "1750492712", "data": [...] }, "primary_type": "CreatePaymentsForwardRequest" } } ``` ### 2. Execute Payment **PUT** `${this.baseUrl}/v2/payments` Executes the payment using signed typed data. ```bash theme={null} curl -X PUT "${this.baseUrl}/v2/payments" \ -H "Authorization: Bearer YOUR_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "signer": "0x1234567890abcdef...", "from": "te-abc123def456", "to": [ { "to": "us-xyz789abc123", "amount_cents": 100000, "currency_symbol": "USD", "invoice_description": "Salary payment for January 2024" } ], "pay_now": true, "network": "arbitrum", "typed_data": {...}, "signature": "0x..." }' ``` ```json theme={null} { "success": true, "data": { "transaction": "tx-abc123def456", "payments": [ { "id": "pay-xyz789abc123", "groupID": "group-123", "payAtTime": "1704067200", "validMinutes": "1440", "payType": "immediate", "token": "0x...", "recipient": "0x...", "amount": "1000000000000000000", "data": "0x..." } ] } } ``` ### 3. Query Payments **GET** `${this.baseUrl}/v2/payments` Query payments for a team with optional filtering. ```bash theme={null} curl -X GET "${this.baseUrl}/v2/payments?team_nanoid=te-abc123def456&state=all&query_type=payable&start_date=2024-01-01&end_date=2024-01-31" \ -H "Authorization: Bearer YOUR_JWT_TOKEN" ``` ## API Integration Example ```javascript theme={null} import { ethers } from 'ethers'; class RisePayments { constructor(baseUrl, jwtToken) { this.baseUrl = baseUrl; this.headers = { 'Authorization': `Bearer ${jwtToken}`, 'Content-Type': 'application/json' }; } async createPayment( teamNanoid, recipients, walletAddress, privateKey, payNow = true ) { // Step 1: Create payment draft const createResponse = await fetch(`${this.baseUrl}/v2/payments`, { method: 'POST', headers: this.headers, body: JSON.stringify({ from: teamNanoid, to: recipients, pay_now: payNow, network: 'arbitrum', }), }); if (!createResponse.ok) { const errorText = await createResponse.text(); throw new Error( `Failed to create payment draft: ${createResponse.status} - ${errorText}` ); } const { data: typedData } = await createResponse.json(); console.log('Payment draft created:', typedData); // Step 2: Sign typed data const wallet = new ethers.Wallet(privateKey); const signature = await wallet.signTypedData( typedData.domain, typedData.types, typedData.typed_data ); // Step 3: Execute payment const executeResponse = await fetch(`${this.baseUrl}/v2/payments`, { method: 'PUT', headers: this.headers, body: JSON.stringify({ signer: walletAddress, from: teamNanoid, to: recipients, pay_now: payNow, typed_data: typedData.typed_data, signature: signature, }), }); const response = await executeResponse.json(); console.log('Payment executed:', response.data); return response.data; } async queryPayments(teamNanoid, options = {}) { const params = new URLSearchParams({ team_nanoid: teamNanoid, state: options.state || 'all', query_type: options.queryType || 'payable', ...(options.startDate && { start_date: options.startDate }), ...(options.endDate && { end_date: options.endDate }), ...(options.recipient && { recipient: options.recipient }) }); const response = await fetch(`${this.baseUrl}/v2/payments?${params}`, { headers: this.headers }); return response.json(); } } // Usage example const paymentsApi = new RisePayments( 'your-base-url', // Configure for your environment 'your-jwt-token' ); const to = [ { to: 'us-jRxg2LRL54DJ', amount_cents: 300, currency_symbol: 'USD', invoice_description: 'Papa Sent you', }, { to: 'us-d6JHBF2kuZjE', amount_cents: 700, currency_symbol: 'USD', invoice_description: 'Papa Sent you', }, ]; const payment = await paymentsApi.createPayment( 'te-bXy7gjb_Iga-', to, process.env.WALLET_ADDRESS, process.env.WALLET_PRIVATE_KEY, true // true = pay immediately, false = pay intent (pay later) ); ``` ## Payment Data Structure ### Payment Request ```json theme={null} { "from": "te-abc123def456", "to": [ { "to": "us-xyz789abc123", "amount_cents": 100000, "currency_symbol": "USD", "invoice_description": "Optional description" } ], "pay_now": true, "network": "arbitrum" } ``` ### Query Parameters | Parameter | Type | Required | Description | | ------------- | ------ | -------- | --------------------------------------------------------------- | | `team_nanoid` | string | Yes | Team identifier | | `state` | string | No | Payment state filter (`all`, `intent`, `scheduled`, `complete`) | | `query_type` | string | Yes | Query type (`payable`, `receivable`) | | `start_date` | string | No | Start date for filtering (YYYY-MM-DD) | | `end_date` | string | No | End date for filtering (YYYY-MM-DD) | | `recipient` | string | No | Filter by specific recipient | ## Payment Timing Rise supports two payment timing options: | Option | Description | Use Case | | ---------------- | ---------------------------------------------------- | --------------------------------------------------- | | `pay_now: true` | Payment executes immediately | Instant payroll, urgent payments | | `pay_now: false` | Payment intent — requires approval before it is sent | Payments that need sign-off from a payment executor | ## Payment States Payments go through several states during processing: | State | Description | Next Actions | | ----------- | ------------------------------- | -------------------------- | | `intent` | Payment intent created | Sign and execute | | `scheduled` | Payment scheduled for future | Wait for execution time | | `complete` | Payment completed on blockchain | Payment successful | | `removed` | Payment cancelled or failed | Review and retry if needed | ## Error Handling Common payment errors and solutions: | Error | Description | Solution | | ------------------------------ | ------------------------------------ | -------------------------------- | | `INSUFFICIENT_BALANCE` | Not enough funds in entity balance | Add funds to the entity balance | | `INVALID_SIGNATURE` | Signature verification failed | Ensure correct wallet is signing | | `EXPIRED_TYPED_DATA` | Typed data has expired | Request fresh typed data | | `INVALID_RECIPIENT` | Recipient address is invalid | Verify recipient nanoid exists | | `TEAM_ROLE_SETTINGS_NOT_FOUND` | User not properly configured in team | Check team role settings | ## Best Practices **Always review payment details before signing** to prevent unauthorized transactions. * **Verify Recipients**: Double-check recipient addresses and amounts * **Monitor Balances**: Ensure sufficient funds before creating payments * **Track Status**: Monitor payment status through the query endpoint * **Handle Errors**: Implement proper error handling for failed payments * **Security**: Use HTTPS and verify contract addresses * **Rate Limiting**: Implement appropriate rate limiting for payment queries * **Wallet Security**: Keep private keys secure and never expose them in client-side code ## Related Resources Complete payment integration examples Wallet integration and signing Managing entity balances Team payment permissions **Payment Precision**: All payment amounts are specified in cents to maintain precision. Convert to decimal format only when displaying to users. # RiseID Source: https://docs.riseworks.io/concepts/riseid Understanding RiseID as the core identity system in the Rise ecosystem RiseID is the core identity system in the Rise ecosystem. Every user, team, and company gets its own RiseID once registered/created in the platform. The RiseID contract is deployed by Rise and all fees are paid by Rise - users do not have to pay any fees for contract deployment. ## What is RiseID? A RiseID is a unique 42-character identifier that represents an entity in the Rise ecosystem. It follows the Ethereum address format: `0x` followed by 40 hexadecimal characters (e.g., `0x1234567890abcdef1234567890abcdef12345678`). ### RiseID Contract Structure The RiseID contract can be thought of as a **collection of wallets**, where every wallet has a role inside the contract. This role determines what the wallet can/cannot do within the RiseID. Deployed with the user's RSK as owner role
Hardware used during onboarding controls the contract
Deployed with the user RiseID as owner
All wallets in user RiseID can control company RiseID
Deployed with its owner company as owner
All wallets in user and company RiseIDs can control team RiseID
## RiseID Hierarchy The RiseID system implements a hierarchical permission structure: ``` User RiseID (Owner: User's RSK) ↓ Company RiseID (Owner: User RiseID) ↓ Team RiseID (Owner: Company RiseID) ``` ### Permission Bubbling When a user wants to run a transaction in a Team RiseID, they sign with their RSK. Since the RSK is not directly added to the team RiseID, the team RiseID will "bubble up" the permission check through the hierarchy: 1. **Team RiseID** checks if RSK has permission → Not found 2. **Company RiseID** checks if RSK has permission → Not found 3. **User RiseID** validates the RSK as a valid owner → **Permission granted** This hierarchical structure allows for efficient permission management across the entire organization. ## RiseID Contract Features ### ERC725X and ERC725Y Implementation The RiseID contract implements **ERC725X** and **ERC725Y** standards, which means: * **ERC725X**: The owner can use the RiseID as a storage for any kind of information using key/value pairs * **ERC725Y**: Supports calling arbitrary transactions through its `call` and `execute` functions ### Arbitrary Transaction Execution Wallets controlling a RiseID can: * Encode other contract calls * Use the RiseID to execute operations * Store and retrieve data using key/value pairs ## RiseID Types and Usage ### RiseID vs Nanoid Comparison | Aspect | RiseID | Nanoid | | ----------- | -------------------------------------------- | ---------------------------------- | | **Format** | 42-character Ethereum address | Variable length string with prefix | | **Usage** | Authentication and contract control | API operations and balance queries | | **Example** | `0x1234567890abcdef1234567890abcdef12345678` | `te-abc123def456` | | **Purpose** | On-chain identity and permissions | API endpoint identifiers | ### When to Use RiseID * **Authentication**: SIWE authentication flow * **Contract Operations**: Smart contract interactions * **Permission Management**: Role-based access control * **On-chain Identity**: Blockchain-based identity verification **For SIWE authentication, use your personal (user) RiseID as a bare `0x` address.** It's shown on your **My Profile** page. Strip any network prefix (`arb4:`), and don't send a company or team RiseID, a `us-`/`co-`/`te-` nanoid, or your Rise Account address. Each of those returns `404 No entity found with riseid`. See [Finding your RiseID](/authentication/api-access#finding-your-riseid). ### When to Use Nanoid * **API Operations**: Most API endpoints * **Balance Queries**: Entity balance lookups * **Team Management**: Team-related operations * **Payment Processing**: Payment creation and management ## RiseID Recovery ### Recovery Process RiseIDs are controlled by user-owned wallets. If a user loses access to their wallets, they become locked out of their RiseID and cannot perform actions like payments or withdrawals. **Recovery Solution**: Rise can recover a RiseID by assigning it a new owner address chosen by the user. ### Recovery Steps 1. **Contact Support**: User contacts Rise support when locked out 2. **Identity Verification**: Support verifies user identity 3. **New Owner Assignment**: Rise assigns a new owner address 4. **Access Restoration**: User regains control of their RiseID **Important**: Always maintain secure backup of your wallet credentials. Recovery is available but should be used as a last resort. ## Security Considerations ### RiseID Security * **42-character format**: RiseID must be exactly 42 characters (Ethereum address format) * **Contract ownership**: Only authorized wallets can control RiseID contracts * **Hierarchical permissions**: Permission checks bubble up through the hierarchy * **Recovery mechanism**: Support can help recover lost access ### Best Practices * **Secure wallet storage**: Use hardware wallets for high-value operations * **Permission management**: Regularly review and update wallet permissions * **Backup strategies**: Maintain secure backups of wallet credentials * **Support contact**: Keep support contact information readily available ## Code Examples ### RiseID Format Validation ```javascript theme={null} function validateRiseID(riseid) { // RiseID must be exactly 42 characters (0x + 40 hex chars) const riseidRegex = /^0x[a-fA-F0-9]{40}$/; return riseidRegex.test(riseid) && riseid.length === 42; } // Example usage const riseid = "0x1234567890abcdef1234567890abcdef12345678"; console.log(validateRiseID(riseid)); // true ``` ### RiseID vs Nanoid Usage ```javascript theme={null} // Authentication - Use RiseID const authenticate = async (walletAddress, riseid) => { const response = await fetch( `/v2/auth/siwe?wallet=${walletAddress}&riseid=${riseid}` ); return response.json(); }; // Balance query - Use Nanoid const getBalance = async (nanoid) => { const response = await fetch(`/v2/balance?nanoid=${nanoid}`); return response.json(); }; ``` ## Related Concepts Understanding balance management with nanoid SIWE authentication with RiseID Team management and permissions Role-based access control # Roles & Permissions Source: https://docs.riseworks.io/concepts/roles Understanding role-based access control and permissions in Rise B2B API Rise B2B API implements a comprehensive role-based access control (RBAC) system that defines what users can do within companies and teams. Understanding these roles is essential for building secure integrations. ## Role Hierarchy Rise uses a hierarchical permission system where company roles cascade down to team roles: Organization-wide roles that control company settings and team access Team-specific roles that control team operations and member management ## Company Roles Company roles provide organization-wide permissions and automatically cascade to teams within the company. ### Company Owner (`company`) * **Full control** over the entire company * Can manage all company settings and configurations * Controls all teams and their members * Can assign and revoke any role within the company * Access to all company features and data ### Organization Admin (`org_admin`) * **Administrative control** over company operations * Can manage company settings and team configurations * Can invite and manage team members across all teams * Controls company-wide payment and financial settings * Cannot modify company ownership or core structure ### Organization Finance Admin (`org_finance_admin`) * **Financial management** capabilities across all teams * Can create and execute payments for any team * Manages company budgets and spending limits * Can view financial reports and transaction history * Cannot modify team structure or member permissions ### Organization Viewer (`org_viewer`) * **Read-only access** to company information * Can view company details, teams, and financial data * Cannot create payments or modify any settings * Useful for auditors, accountants, or stakeholders ## Team Roles Team roles provide granular control over specific team operations and member management. ### Team Admin (`team_admin`) * **Full control** over team settings and members * Can create, modify, and delete team configurations * Manages team funds and payment limits * Can invite and remove team members * Access to all team features and data ### Team Finance Admin (`team_finance_admin`) * **Financial management** capabilities for the team * Can create and execute payments for the team * Manages team budgets and spending limits * Can view team financial reports and transaction history * Cannot modify team structure or invite new members ### Team Employee (`team_employee`) * **Standard team member** with payment capabilities * Can create and execute payments within team limits * Access to team funds for authorized transactions * Can view relevant team information * Cannot modify team settings or invite new members ### Team Viewer (`team_viewer`) * **Read-only access** to team information * Can view team members, payments, and balances * Cannot create payments or modify any settings * Useful for auditors, accountants, or stakeholders ### Contractor (`contractor`) * **External contractor** with limited team access * Can receive payments from the team * Limited access to team information * Cannot create payments or modify team settings ## Role Permissions Matrix ### Company-Level Permissions | Permission | Company Owner | Org Admin | Org Finance Admin | Org Viewer | | --------------------------- | ------------- | --------- | ----------------- | ---------- | | **View Company** | ✅ | ✅ | ✅ | ✅ | | **Manage Company Settings** | ✅ | ✅ | ❌ | ❌ | | **Manage Teams** | ✅ | ✅ | ❌ | ❌ | | **Company Payments** | ✅ | ✅ | ✅ | ❌ | | **Invite Members** | ✅ | ✅ | ❌ | ❌ | | **Manage Roles** | ✅ | ✅ | ❌ | ❌ | | **View Financial Data** | ✅ | ✅ | ✅ | ✅ | ### Team-Level Permissions | Permission | Team Admin | Finance Admin | Employee | Viewer | Contractor | | ------------------------ | ---------- | ------------- | -------- | ------ | ---------- | | **View Team** | ✅ | ✅ | ✅ | ✅ | ✅ | | **Manage Team Settings** | ✅ | ❌ | ❌ | ❌ | ❌ | | **Team Payments** | ✅ | ✅ | ✅ | ❌ | ❌ | | **Invite Members** | ✅ | ❌ | ❌ | ❌ | ❌ | | **Manage Roles** | ✅ | ❌ | ❌ | ❌ | ❌ | | **View Financial Data** | ✅ | ✅ | ✅ | ✅ | ❌ | ## Role Hierarchy & Cascading ### Company to Team Role Mapping When a user has a company role, it automatically grants them corresponding team roles:
  • companyteam\_admin
  • org\_adminteam\_admin
  • org\_finance\_adminteam\_finance\_admin
  • org\_viewerteam\_viewer
Company roles automatically grant equivalent team permissions across all teams in the organization
## How Roles Work in the API ### Permission Validation The B2B API uses **permission validation** rather than role queries. When you make API calls, the system automatically checks if your user has the required permissions. If you lack permission, the API returns a 403 Forbidden error with details about what role is required. ### Error-Based Permission Discovery Since there's no direct role query endpoint, you can discover user permissions through API responses. When an API call fails with a 403 error, the response will indicate what role is required for that operation. ## Role Management ### Assigning Roles Roles can be assigned through the invite system using the `/v2/invites/manager` endpoint. ### Role Settings You can access role-specific settings for team members using the team member settings endpoints. ## Security Best Practices * Assign the minimum role required for each user * Regularly review and audit role assignments * Remove unnecessary permissions promptly * Rotate admin roles regularly * Have multiple admins for critical operations * Document role changes for audit trails * Monitor role assignments and changes * Log all permission-related activities * Set up alerts for suspicious role changes * Conduct regular permission audits * Review inactive user permissions * Validate role assignments against business needs ## Error Handling Common role-related errors and solutions: | HTTP Status | Error | Description | Solution | | ----------- | -------------------------- | -------------------------------------------- | ----------------------------------------------- | | `403` | `INSUFFICIENT_PERMISSIONS` | User lacks required role | Check user's current role and upgrade if needed | | `400` | `INVALID_ROLE` | Role doesn't exist or is invalid | Use one of the valid role types | | `404` | `USER_NOT_FOUND` | User doesn't exist in the entity | Verify user is a member of the entity | | `409` | `ROLE_CONFLICT` | Role assignment conflicts with existing role | Remove existing role before assigning new one | ### Common Error Scenarios **Insufficient Permissions (403):** ```json theme={null} { "success": false, "data": "User us-abc123def456 must have admin access to team te-xyz789abc123. Current role: team_viewer" } ``` **Invalid Role Assignment (400):** ```json theme={null} { "success": false, "data": "Invalid role: super_admin. Valid roles are: team_admin, team_finance_admin, team_viewer" } ``` ## Real-World Role Scenarios ### Startup Organization **Company Level:** * **Founder**: `company` role with full control * **Co-founder**: `org_admin` role for administrative control * **Accountant**: `org_finance_admin` role for financial management * **Investor**: `org_viewer` role for read-only access **Team Level:** * **Engineering Lead**: `team_admin` role for team management * **Senior Developer**: `team_employee` role for standard operations * **Intern**: `team_viewer` role for limited access ### Enterprise Organization **Company Level:** * **CEO**: `company` role with full control * **CTO**: `org_admin` role for technical operations * **CFO**: `org_finance_admin` role for financial management * **Auditor**: `org_viewer` role for compliance monitoring **Department Level:** * **Engineering Director**: `team_admin` role for engineering team * **Finance Manager**: `team_finance_admin` role for finance team * **HR Director**: `team_admin` role for HR team ## Related Concepts Understanding team management Balance management and permissions SIWE authentication and role validation Role assignment through invites **Role Discovery**: Since there's no direct role query endpoint, you can discover user permissions by attempting API calls and handling 403 errors appropriately. The API will automatically validate permissions for each operation. # Teams Source: https://docs.riseworks.io/concepts/teams Learn about team management, roles, and permissions in Rise B2B Teams are the core organizational unit in Rise B2B. They represent groups of users who can collaborate on payments, manage shared funds, and control access to resources. ## Team Structure Establish a new team with a name and initial admin Invite users with specific roles and permissions Control shared entity balances and payment limits Configure access levels for different team members ## Team Roles Rise B2B supports four distinct team roles with different permission levels: ### Team Admin * **Full control** over team settings and members * Can create, modify, and delete team configurations * Manages all team funds and payment limits * Can invite and remove team members * Access to all team features and data ### Finance Admin * **Financial management** capabilities * Can create and execute payments * Manages team budgets and spending limits * Can view financial reports and transaction history * Cannot modify team structure or member permissions ### Team Employee * **Standard team member** with payment capabilities * Can create and execute payments within limits * Access to team funds for authorized transactions * Can view relevant team information * Cannot modify team settings or invite new members ### Team Viewer * **Read-only access** to team information * Can view team members, payments, and balances * Cannot create payments or modify any settings * Useful for auditors, accountants, or stakeholders ## Creating Teams ### Step 1: Create a New Team ```bash theme={null} curl -X POST "${this.baseUrl}/v2/teams" \ -H "Authorization: Bearer YOUR_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Engineering Team", "description": "Core engineering team for product development", "entity_nanoid": "entity_123", "admin_wallet": "0x1234567890abcdef..." }' ``` ```json theme={null} { "success": true, "data": { "team_nanoid": "team_123456789", "name": "Engineering Team", "description": "Core engineering team for product development", "entity_nanoid": "entity_123", "admin_wallet": "0x1234567890abcdef...", "created_at": "2024-01-01T12:00:00Z", "member_count": 1, "status": "active" } } ``` ### Step 2: Add Team Members ```bash theme={null} curl -X POST "${this.baseUrl}/v2/teams/team_123456789/members" \ -H "Authorization: Bearer YOUR_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "user_nanoid": "user_456", "role": "team_employee", "permissions": ["pay", "view"] }' ``` ## Team Management Examples ```javascript theme={null} class RiseTeams { constructor(baseUrl, jwtToken) { this.baseUrl = baseUrl; this.headers = { 'Authorization': `Bearer ${jwtToken}`, 'Content-Type': 'application/json' }; } async createTeam(name, description, entity_nanoid, admin_wallet) { const teamData = { name, description, entity_nanoid: entity_nanoid, admin_wallet: admin_wallet }; const response = await fetch(`${this.baseUrl}/v2/teams`, { method: 'POST', headers: this.headers, body: JSON.stringify(teamData) }); return response.json(); } async getTeam(teamNanoid) { const response = await fetch( `${this.baseUrl}/v2/teams/${teamNanoid}`, { headers: this.headers } ); return response.json(); } async addMember(teamNanoid, userNanoid, role, permissions) { const memberData = { user_nanoid: userNanoid, role, permissions }; const response = await fetch(`${this.baseUrl}/v2/teams/${teamNanoid}/members`, { method: 'POST', headers: this.headers, body: JSON.stringify(memberData) }); return response.json(); } async updateMemberRole(teamNanoid, userNanoid, newRole, newPermissions) { const updateData = { role: newRole, permissions: newPermissions }; const response = await fetch(`${this.baseUrl}/v2/teams/${teamNanoid}/members/${userNanoid}`, { method: 'PUT', headers: this.headers, body: JSON.stringify(updateData) }); return response.json(); } async removeMember(teamNanoid, userNanoid) { const response = await fetch(`${this.baseUrl}/v2/teams/${teamNanoid}/members/${userNanoid}`, { method: 'DELETE', headers: this.headers }); return response.json(); } } // Usage example const teamsApi = new RiseTeams( 'https://b2b-api.riseworks.io', // or your environment URL 'your-jwt-token' ); ``` ```python theme={null} import requests from typing import List, Optional class RiseTeams: def __init__(self, base_url: str, jwt_token: str = None): self.base_url = base_url self.headers = { "Authorization": f"Bearer {jwt_token}", "Content-Type": "application/json" } def create_team( self, name: str, description: str, entity_nanoid: str, admin_wallet: str ) -> dict: """Create a new team""" team_data = { "name": name, "description": description, "entity_nanoid": entity_nanoid, "admin_wallet": admin_wallet } response = requests.post( f"{self.base_url}/v2/teams", headers=self.headers, json=team_data ) return response.json() def get_team(self, team_nanoid: str) -> dict: """Get team details""" response = requests.get( f"{self.base_url}/v2/teams/{team_nanoid}", headers=self.headers ) return response.json() def add_member( self, team_nanoid: str, user_nanoid: str, role: str, permissions: List[str] ) -> dict: """Add a member to the team""" member_data = { "user_nanoid": user_nanoid, "role": role, "permissions": permissions } response = requests.post( f"{self.base_url}/v2/teams/{team_nanoid}/members", headers=self.headers, json=member_data ) return response.json() # Usage example teams_api = RiseTeams( "https://b2b-api.riseworks.io", # or your environment URL "your-jwt-token" ) ``` ## Team Permissions Matrix | Permission | Team Admin | Finance Admin | Employee | Viewer | | ----------- | ---------- | ------------- | -------- | ------ | | **View** | ✅ | ✅ | ✅ | ✅ | | **Pay** | ✅ | ✅ | ✅ | ❌ | | **Finance** | ✅ | ✅ | ❌ | ❌ | | **Manage** | ✅ | ❌ | ❌ | ❌ | | **Invite** | ✅ | ❌ | ❌ | ❌ | ## Error Handling Common team management errors and solutions: | Error Code | Description | Solution | | -------------------------- | -------------------------------- | ----------------------------------------------- | | `INVALID_TEAM_NAME` | Team name is invalid or too long | Use a valid team name (2-50 characters) | | `INVALID_ADMIN_WALLET` | Admin wallet address is invalid | Ensure wallet address is valid Ethereum address | | `INSUFFICIENT_PERMISSIONS` | User lacks permission for action | Check user's role and permissions | | `TEAM_NOT_FOUND` | Team does not exist | Verify team\_nanoid is correct | | `MEMBER_ALREADY_EXISTS` | User is already a team member | Check existing team membership | | `CANNOT_REMOVE_ADMIN` | Cannot remove the last admin | Ensure at least one admin remains | ## Best Practices **Always maintain at least one team admin** to prevent losing access to team management capabilities. * **Role Hierarchy**: Use the principle of least privilege when assigning roles * **Regular Audits**: Periodically review team members and their permissions * **Documentation**: Keep records of team structure and permission changes * **Backup Admins**: Always have multiple admins for critical teams * **Permission Reviews**: Regularly review and update member permissions See [Permissions](/concepts/permissions) for details on role-based access control. # Wallets & Private Keys Source: https://docs.riseworks.io/concepts/wallets Understanding digital wallets and private keys for Rise authentication # Wallets & Private Keys Digital wallets and private keys are fundamental concepts for authenticating with Rise. This guide explains how they function in accessible terms. A digital wallet stores your private keys and manages your digital identity A private key is like a digital signature that proves you own your wallet Rise uses your wallet to verify your identity through secure signing Keep your private keys secure and never share them with anyone ## What is a Digital Wallet? A **digital wallet** is software that stores your private keys and manages your digital identity on the blockchain. It functions as a secure digital bank account that you control. ### Key Features: * **Stores private keys** securely * **Manages your digital identity** (wallet address) * **Signs transactions** to prove ownership * **Connects to applications** like Rise ### Popular Wallet Types: * **Software wallets** - Digital wallets stored on your device * **Hardware wallets** - Physical devices that store your private keys offline * **Mobile wallets** - Wallet applications on your smartphone ### Ways to Access Your Wallet: * **Browser extensions** - Like MetaMask, which provides a browser interface * **Desktop applications** - Standalone wallet software * **Mobile apps** - Wallet applications on iOS/Android * **Hardware device interfaces** - Software to interact with hardware wallets ## What is a Private Key? A **private key** is a long string of letters and numbers that verifies your wallet ownership. It functions as a master password that provides complete control over your digital identity. ### Private Key Format: ``` 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef ``` ### Key Characteristics: * **64 characters long** (32 bytes) * **Starts with "0x"** (hexadecimal format) * **Unique to your wallet** - no two wallets have the same private key * **Must be kept secret** - anyone with your private key can control your wallet **Never share your private key!** It gives complete access to your wallet and all associated accounts. Store it securely and use environment variables in production. ## How Rise Authentication Works Rise uses your wallet and private key to authenticate you securely: ### 1. **Wallet Connection** Your wallet address (derived from your private key) is connected to your Rise account. ### 2. **Secure Signing** When you execute API calls, Rise requests your wallet to sign a message using your private key. ### 3. **Identity Verification** The signature verifies your wallet ownership, which verifies your Rise account ownership. ### 4. **Access Granted** Rise grants you access to your account and enables you to perform operations. ## Getting Your Private Key ### From MetaMask: 1. Open MetaMask extension 2. Click the account menu (three dots) 3. Go to "Account details" 4. Click "Export Private Key" 5. Enter your MetaMask password 6. Copy the private key (starts with 0x) ### From Hardware Wallet: 1. Connect your hardware wallet 2. Open the wallet software 3. Navigate to "Export" or "Private Key" 4. Follow the device-specific instructions 5. Copy the private key **Security Recommendation**: Consider using a secondary wallet for API operations. Learn more in our [Secondary Wallets Guide](/security/secondary-wallets). ## Wallet Address vs Private Key | Wallet Address | Private Key | | --------------------------------------------------------- | -------------------------------------------- | | **Public identifier** - like an email address | **Secret key** - like a password | | **Safe to share** - used to receive funds | **Never share** - gives full control | | **Derived from private key** | **Generates the wallet address** | | **Example**: `0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6` | **Example**: `0x1234...abcd` (64 characters) | ## Security Best Practices ### Recommended Practices: * **Store private keys securely** in environment variables * **Use hardware wallets** for maximum security * **Create secondary wallets** for API operations * **Backup your private keys** safely * **Use strong passwords** for wallet software ### Security Considerations: * **Never share private keys** with anyone * **Never store private keys** in code or version control * **Avoid using main wallets** with significant funds for API operations * **Never forget to backup** your private keys * **Never use weak passwords** for wallet software ## Common Questions ### "Do I need to understand blockchain to use Rise?" **No!** Rise handles all the blockchain complexity for you. You can use a private key from your wallet for authentication. ### "Is my private key the same as my Rise ID?" **No!** Your Rise ID is your Rise account identifier. Your private key is for your wallet, which is connected to your Rise account. ### "What if I lose my private key?" **Contact Rise support immediately.** You may need to create a new wallet and update your Rise account connection. ### "Can I use the same private key for multiple accounts?" **Yes**, but it's not recommended for security reasons. Each wallet should ideally be used for one purpose. ## Next Steps Learn about different authentication methods Detailed guide on getting and using private keys Learn about using dedicated wallets for API operations Comprehensive security guidelines **Ready to begin your integration?** Review our [Quickstart Guide](/quickstart) to start integrating with Rise using your wallet and private key. # Auth Integration Guide Source: https://docs.riseworks.io/guides/auth-integration End-to-end authentication flow with Rise SDK This guide walks you through implementing authentication with Rise using the official SDK. The Rise SDK handles all the complexity of SIWE and JWT authentication automatically. ## Authentication Flow Install the Rise SDK package Set up SDK with Rise ID and private key or JWT token SDK handles SIWE signing and JWT token management Execute API calls with automatic authentication ## SDK Installation First, install the Rise SDK: ```bash theme={null} npm install @riseworks/sdk # Or: yarn add @riseworks/sdk # Or: pnpm add @riseworks/sdk ``` ## Complete Integration Example ```javascript theme={null} const { RiseApiClient } = require('@riseworks/sdk'); require('dotenv').config(); // Initialize with Rise ID and private key (recommended) const client = new RiseApiClient({ environment: 'prod', riseIdAuth: { riseId: process.env.RISE_ID, privateKey: process.env.PRIVATE_KEY } }); // Alternative: Initialize with JWT token const jwtClient = new RiseApiClient({ environment: 'prod', jwtToken: process.env.JWT_TOKEN }); async function main() { try { // SDK automatically handles authentication console.log('Authenticating with Rise...'); // Get user information const user = await client.me.get(); console.log('Authenticated as:', user.data.name); // Get user's teams const teams = await client.me.teams(); console.log('Teams:', teams.data); // Get company information const company = await client.company.get(); console.log('Company:', company.data.name); } catch (error) { console.error('Authentication error:', error.message); } } // Run the example if (require.main === module) { main(); } ``` ## Authentication Methods ### Method 1: Rise ID Authentication (Recommended) Use your Rise ID and wallet private key for automatic authentication: ```javascript theme={null} const { RiseApiClient } = require('@riseworks/sdk'); const client = new RiseApiClient({ environment: 'prod', riseIdAuth: { riseId: process.env.RISE_ID, privateKey: process.env.PRIVATE_KEY } }); // SDK automatically handles SIWE and JWT token generation const user = await client.me.get(); ``` **Benefits:** * **Automatic JWT generation** - SDK handles SIWE signing and JWT token creation * **Token renewal** - SDK automatically refreshes expired tokens * **Full API access** - Access to all API endpoints including sensitive operations * **Simplified integration** - No manual authentication code required ### Method 2: JWT Authentication Use a pre-generated JWT token for direct API access: ```javascript theme={null} const { RiseApiClient } = require('@riseworks/sdk'); const client = new RiseApiClient({ environment: 'prod', jwtToken: process.env.JWT_TOKEN }); // SDK automatically includes JWT in all requests const user = await client.me.get(); ``` **Use cases:** * **Existing JWT tokens** - When you already have a valid JWT token * **Read-only operations** - For applications that only need to read data * **Simple integrations** - When you don't need sensitive write operations ## Error Handling The SDK provides comprehensive error handling with descriptive error messages: ```javascript theme={null} try { const user = await client.me.get(); console.log('Success:', user.data); } catch (error) { console.error('Authentication error:', error.message); // Handle specific error types based on message content if (error.message.includes('Failed to generate JWT token')) { console.error('JWT generation failed. Check your Rise ID and private key.'); } else if (error.message.includes('401')) { console.error('Authentication failed. The SDK will automatically retry.'); } else if (error.message.includes('403')) { console.error('Insufficient permissions for this operation.'); } else { console.error('API Error:', error.message); } } ``` Common authentication errors: | Error Message | Description | Solution | | ------------------------------------------ | ----------------------------- | -------------------------------------------- | | `Failed to generate JWT token` | JWT generation failed | Check Rise ID and private key | | `Rise ID and private key are required` | Missing credentials | Provide both Rise ID and private key | | `Invalid Rise ID address` | Rise ID format is invalid | Verify Rise ID format (0x + 40 hex chars) | | `Private key should be a valid hex string` | Private key format is invalid | Check private key format (0x + 64 hex chars) | | `HTTP 401` | Authentication failed | SDK automatically handles JWT refresh | | `HTTP 403` | Insufficient permissions | Check user roles and permissions | ## Security Best Practices **Never expose private keys** in client-side code or commit them to version control. ### Secondary Wallets ```javascript theme={null} // Use secondary wallets for API operations // Never use wallets with significant funds for API authentication const client = new RiseApiClient({ environment: 'prod', riseIdAuth: { riseId: process.env.RISE_ID, privateKey: process.env.SECONDARY_WALLET_PRIVATE_KEY // Use secondary wallet } }); ``` ### Error Logging ```javascript theme={null} // Log authentication events for security auditing client.on('auth', (event) => { console.log('Authentication event:', event.type, event.timestamp); }); ``` ## Troubleshooting ### Common Issues 1. **"Authentication failed"** * Verify Rise ID and private key are correct * Ensure wallet has sufficient funds for gas fees * Check network connectivity 2. **"JWT token expired"** * SDK automatically handles token renewal * If issues persist, reinitialize the client 3. **"Insufficient permissions"** * Check user roles and permissions * Verify team membership for team-specific operations 4. **"Network error"** * Check internet connection * Verify API endpoint is accessible * Check firewall settings ## Next Steps Learn how to process payments with automatic authentication Manage teams and permissions with the SDK Set up real-time notifications for your integration Comprehensive error handling and troubleshooting **Need help?** See the [Authentication](/authentication) page for detailed documentation or contact support at [Hello@Riseworks.io](mailto:Hello@Riseworks.io) # Error Handling Source: https://docs.riseworks.io/guides/error-handling Error codes and troubleshooting for Rise B2B API # Error Handling Rise B2B API returns clear error codes and messages for all endpoints. This guide helps you understand and handle errors effectively in your integrations. Common authentication and authorization issues HTTP status codes and API-specific errors SDK-specific error handling and troubleshooting Error handling best practices and patterns ## Common Error Codes | HTTP Status | Description | Common Causes | | ----------- | --------------------- | ------------------------------------------ | | `400` | Bad Request | Missing or invalid parameters | | `401` | Unauthorized | Invalid JWT token or SIWE signature | | `403` | Forbidden | Insufficient permissions for the operation | | `404` | Not Found | Resource doesn't exist | | `429` | Too Many Requests | Rate limit exceeded | | `500` | Internal Server Error | Server-side error | ## Authentication Errors ### SIWE Authentication Errors | Error | Cause | Solution | | -------------------------------------- | ----------------------------- | ----------------------------------------------------------------------------- | | `Rise ID and private key are required` | Missing SIWE credentials | Configure `RISE_ID` and `WALLET_PRIVATE_KEY` in environment, or use JWT token | | `Invalid Rise ID address` | Incorrect Rise ID format | Ensure Rise ID starts with 0x and is 42 characters | | `Invalid private key` | Incorrect private key format | Ensure private key starts with 0x and is 66 characters | | `JWT generation failed` | Authentication failed | Verify your Rise ID and private key match your Rise account | | `SIWE error: Invalid signature` | Signature verification failed | Verify you're using the correct wallet and signing the exact message | | `SIWE error: Expired nonce` | Nonce has expired | Retrieve a new SIWE message and sign it immediately | ### JWT Authentication Errors | Error | Cause | Solution | | ----------------------------------- | ---------------------------- | ------------------------------------------------ | | `401 Unauthorized` | JWT token expired or invalid | Refresh your JWT token or re-authenticate | | `Invalid JWT format` | Malformed JWT token | Verify JWT token format and ensure it's complete | | `JWT signature verification failed` | Token has been tampered with | Retrieve a new JWT token from Rise | ## API Errors ### Common API Error Responses | Error | Cause | Solution | | ----------------------- | -------------------------- | ------------------------------------------------ | | `401 Unauthorized` | Your JWT token has expired | SDK will auto-refresh, or manually refresh token | | `403 Forbidden` | Insufficient permissions | Verify your role permissions for the operation | | `404 Not Found` | Resource doesn't exist | Verify the nanoid or resource identifier | | `400 Bad Request` | Invalid request parameters | Verify all required fields and parameter formats | | `429 Too Many Requests` | Rate limit exceeded | Implement exponential backoff and retry later | ### Resource-Specific Errors | Error | Cause | Solution | | ------------------- | ------------------------------------------ | --------------------------------------------- | | `Team not found` | Invalid team nanoid | Verify the team exists and you have access | | `Company not found` | Invalid company nanoid | Verify company permissions and nanoid | | `User not found` | Invalid user nanoid | Verify user exists in your organization | | `Payment failed` | Insufficient balance or invalid parameters | Verify balance and payment parameters | | `Invalid currency` | Unsupported currency code | Use supported currency codes (USD, EUR, etc.) | ## SDK Error Handling ### SDK-Specific Errors ```javascript theme={null} const { RiseApiClient } = require('@riseworks/sdk'); async function handleSDKErrors() { try { const client = new RiseApiClient({ environment: 'prod', riseIdAuth: { riseId: riseId, privateKey: walletPrivateKey } }); const user = await client.me.get(); console.log('Success:', user.data); } catch (error) { // SDK-specific error handling if (error.message.includes('Rise ID and private key are required')) { console.error('Missing credentials - verify your environment variables'); } else if (error.message.includes('Invalid Rise ID address')) { console.error('Invalid Rise ID format - should be 0x + 40 hex characters'); } else if (error.message.includes('Invalid private key')) { console.error('Invalid private key format - should be 0x + 64 hex characters'); } else if (error.message.includes('401')) { console.error('Authentication failed - verify your credentials'); } else if (error.message.includes('403')) { console.error('Permission denied - verify your role permissions'); } else if (error.message.includes('404')) { console.error('Resource not found - verify the nanoid'); } else if (error.message.includes('429')) { console.error('Rate limit exceeded - implement backoff and retry'); } else { console.error('Unexpected error:', error.message); } } } ``` ### Automatic Error Handling The Rise SDK provides automatic error handling for common scenarios: ```javascript theme={null} // SDK automatically handles JWT refresh const client = new RiseApiClient({ riseIdAuth: { riseId: riseId, privateKey: walletPrivateKey } }); // If JWT expires, SDK automatically generates a new one const user = await client.me.get(); // No manual error handling needed ``` ## Error Handling Best Practices ### Recommended Practices: * **Implement comprehensive error handling** for all API calls * **Log errors with context** for debugging * **Show user-friendly error messages** to end users * **Implement retry logic** with exponential backoff for transient errors * **Monitor error rates** and set up alerts * **Use SDK error handling** when possible ### Security Considerations: * **Never ignore errors** or catch them silently * **Never show technical error messages** to end users * **Never retry indefinitely** without backoff * **Never log sensitive information** like private keys or JWT tokens * **Never assume errors are always client-side** ### Retry Logic Example ```javascript theme={null} async function retryWithBackoff(apiCall, maxRetries = 3) { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { return await apiCall(); } catch (error) { if (attempt === maxRetries) { throw error; // Final attempt failed } // Don't retry on client errors (4xx) if (error.message.includes('400') || error.message.includes('401') || error.message.includes('403') || error.message.includes('404')) { throw error; } // Exponential backoff: 1s, 2s, 4s const delay = Math.pow(2, attempt - 1) * 1000; console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`); await new Promise(resolve => setTimeout(resolve, delay)); } } } // Usage const user = await retryWithBackoff(() => client.me.get()); ``` ## Monitoring and Debugging ### Error Logging ```javascript theme={null} // Structured error logging function logError(error, context = {}) { console.error({ timestamp: new Date().toISOString(), error: error.message, status: error.status || 'unknown', context, stack: error.stack }); } // Usage try { await client.payments.sendPayment(paymentData); } catch (error) { logError(error, { operation: 'sendPayment', teamId: paymentData.team_nanoid }); } ``` ### Common Debugging Steps 1. **Verify Authentication** * Verify Rise ID and private key are correct * Ensure JWT token is valid and not expired * Verify wallet permissions for the Rise account 2. **Validate Parameters** * Verify all required fields are provided * Verify nanoid formats are correct * Ensure currency codes are supported 3. **Verify Permissions** * Verify your role has permission for the operation * Verify team/company access permissions * Ensure you're using the correct Rise account 4. **Network Issues** * Verify internet connectivity * Verify API endpoint is accessible * Verify rate limiting status ## Support Resources Full API specification with error details Authentication troubleshooting SDK error handling examples Get help from our team **Need immediate assistance?** Contact our support team at [Hello@Riseworks.io](mailto:Hello@Riseworks.io) with your error details and we'll help you resolve the issue quickly. # External Entity Payments (Bill Pay) Source: https://docs.riseworks.io/guides/external-entity-payments Pay vendors and recipients who are not Rise users with instant external-entity payments External entity payments let a team pay recipients who are **not** Rise members — vendors, contractors, or suppliers identified by email. Unlike standard payments (which target a Rise member of the team), external-entity payments create or reference an external recipient and disburse funds directly. External entity payments are a team-scoped operation. The signer must hold a wallet with payment permissions on the team's RiseID, exactly like standard payments. ## When to use this Recipient is already a member of your team. Use `client.payments.*` and `POST /v2/payments`. Recipient is an outside vendor/supplier identified by email. Use `client.billPay.*` and the `external_entity` endpoints. ## Payment flow External-entity instant payments follow the same secure prepare → sign → execute pattern as standard payments. The signature is produced locally with your wallet; the private key never leaves your process. Register the external recipient under the team (or reuse an existing one). Request EIP-712 typed data for the instant payment. Sign the returned typed data locally. Submit the signature to execute the payment on-chain. ## SDK usage ### Manage recipients ```ts theme={null} import { RiseApiClient, type TeamNanoid } from '@riseworks/sdk' const client = new RiseApiClient({ environment: 'stg', jwtToken: process.env.RISE_JWT_TOKEN!, }) // List existing external recipients for a team const recipients = await client.billPay.listRecipients({ team_nanoid: 'te_123' as TeamNanoid, }) // Register a new external recipient await client.billPay.createRecipient( { team_nanoid: 'te_123' as TeamNanoid }, { email: 'vendor@example.com' }, ) ``` ### Send an instant payment (one call) `sendInstantPayment` runs the full prepare → sign → execute flow for you. It needs a private key to sign — either passed inline or configured on the client via `riseIdAuth`. ```ts theme={null} const result = await client.billPay.sendInstantPayment({ from: 'te_123', amount_cents: 125_000, // 1,250.00 currency_symbol: 'USD', external_recipient_email: 'vendor@example.com', payment_data: { role_description: 'Design work', invoice_description: 'Invoice INV-2026-001', services_description: 'Landing page design', payment_details: 'Net 15', rise_sow: false, }, privateKey: process.env.RISE_PRIVATE_KEY, // omit if set via riseIdAuth }) console.log(result.data.transaction) ``` ### Prepare and execute separately If you sign with an external wallet (hardware wallet, custody service), split the flow: ```ts theme={null} // 1. Prepare — returns EIP-712 typed data const prepared = await client.billPay.prepareInstantPayment({ from: 'te_123', amount_cents: 125_000, currency_symbol: 'USD', external_recipient_email: 'vendor@example.com', payment_data: { role_description: 'Design work', rise_sow: false }, }) // 2. Sign prepared.data.typed_data with your wallet (returns a signature) // 3. Execute const executed = await client.billPay.executeInstantPayment({ from: 'te_123', amount_cents: 125_000, currency_symbol: 'USD', external_recipient_email: 'vendor@example.com', payment_data: { role_description: 'Design work', rise_sow: false }, signer: walletAddress, typed_data: prepared.data.typed_data, signature, }) ``` ## REST endpoints | Operation | Method & path | | ----------------------- | -------------------------------------------------------- | | List recipients | `GET /v2/payments/teams/{team_nanoid}/external_entity` | | Create recipient | `POST /v2/payments/teams/{team_nanoid}/external_entity/` | | Prepare instant payment | `POST /v2/payments/external_entity/instant` | | Execute instant payment | `PUT /v2/payments/external_entity/instant` | See the [API Reference](/api-reference/introduction) for full request and response schemas. ## Idempotency with `external_id` Payments accept an optional `external_id` — your own reference for the payment (an invoice number, an internal transaction id). Rise enforces that `external_id` is **unique per payer (team)**: a second payment from the same team with an `external_id` it has already used is rejected with `409 Conflict`. Use it to make retries safe and to prevent accidental double-payments. `external_id` uniqueness is enforced **going forward** and is scoped per payer. Omit it (leave it unset) when you do not need idempotency — unset values are never treated as duplicates. Reuse the same `external_id` on a retry to guarantee the payment is only created once. ## Related * [Payment Integration Guide](/guides/payment-integration) — standard payments to team members * [Payments concept](/concepts/payments) — payment flow and on-chain security * [Error Handling](/guides/error-handling) — handling `409` and validation errors # Migrating from V1 to B2B API Source: https://docs.riseworks.io/guides/migrating-from-v1 Complete guide to migrate from Rise V1 API to the new B2B API Welcome to the Rise B2B API! This guide will help you migrate from the V1 API to our new, more powerful B2B API. The B2B API introduces significant improvements in security, functionality, and developer experience. **SDK Recommended**: For the best developer experience with the B2B API, install the Rise SDK: `npm install @riseworks/sdk` (or `yarn add @riseworks/sdk` / `pnpm add @riseworks/sdk`) **Helpful skill**: If you use Claude-style coding skills, add the migration skill with `npx @riseworks/sdk add-skills rise-v1-migration`. SIWE authentication replaces X-Auth-Token for better security Optimized endpoints with improved response times Built for blockchain with EIP-712 typed data signing Enhanced response formats with better error handling ## Key Changes Overview ### Authentication Changes * **V1**: Used SIWE authentication with `/v1/api/siwe` endpoints * **B2B**: Uses **Sign-In with Ethereum (SIWE)** with `/v2/auth/siwe` endpoints * **JWT Tokens**: Still used but obtained through updated SIWE flow ### Endpoint Structure * **V1**: Used `/v1/` prefix with various endpoint patterns * **B2B**: Uses `/v2/` prefix with consistent REST patterns * **Base URL**: Updated to use the new B2B API URLs - see [Environments](/environments) for API URLs ### Data Formats * **V1**: Mixed response formats and error structures * **B2B**: Consistent `{ success: boolean, data: any }` response format * **Error Handling**: Standardized HTTP status codes with detailed error messages ## Endpoint Migration Table ### Authentication Endpoints | V1 Endpoint | B2B Endpoint | | ------------------- | ---------------------- | | `GET /v1/api/siwe` | `GET /v2/auth/siwe` | | `POST /v1/api/siwe` | `POST /v2/auth/verify` | ### Team Management | V1 Endpoint | B2B Endpoint | | --------------------------------------------- | ---------------------------------------------------------- | | `GET /v1/teams` | `GET /v2/user/teams` | | `GET /v1/teams/{teamId}/talent` | `GET /v2/teams/{team_nanoid}/users` | | `GET /v1/teams/{teamId}/talent/{talentId}` | `GET /v2/teams/{team_nanoid}/member/{user_nanoid}/summary` | | `DELETE /v1/teams/{teamId}/talent/{talentId}` | `DELETE /v2/teams/{team_nanoid}/member/{user_nanoid}` | ### Payment Processing | V1 Endpoint | B2B Endpoint | | ------------------------------------- | ---------------------------------------- | | `POST /v1/payments/pay` | `POST /v2/payments` + `PUT /v2/payments` | | `PUT /v1/payments/pay` | `POST /v2/payments` + `PUT /v2/payments` | | `POST /v1/payments/batch-pay` | `POST /v2/payments` + `PUT /v2/payments` | | `PUT /v1/payments/batch-pay` | `POST /v2/payments` + `PUT /v2/payments` | | `POST /v1/payments/batch-pay/intents` | `POST /v2/payments` + `PUT /v2/payments` | | `GET /v1/payments/` | `GET /v2/payments` | ### Balance Management | V1 Endpoint | B2B Endpoint | | ---------------------------------- | --------------------------------- | | `GET /v1/riseid/{rise_id}/balance` | `GET /v2/balance?nanoid={nanoid}` | ### Invites Management | V1 Endpoint | B2B Endpoint | | ------------------------------------------- | ------------------------------------------------------ | | `GET /v1/invites/` | `GET /v2/invites` | | `POST /v1/invites/` (employees/contractors) | `POST /v2/invites` | | `POST /v1/invites/` (managers) | `POST /v2/invites/manager` + `PUT /v2/invites/manager` | | `POST /v1/invites/warmed` | `POST /v2/invites` | ## New B2B API Endpoints (Not in V1) The B2B API introduces several new endpoints that weren't available in V1: ### Team Management (Enhanced) * `GET /v2/teams/{team_nanoid}/settings` - Get team settings * `PUT /v2/teams/{team_nanoid}/settings` - Update team settings * `GET /v2/teams/{team_nanoid}` - Get team by nanoid * `DELETE /v2/teams/{team_nanoid}` - Delete team by nanoid * `PUT /v2/teams/{team_nanoid}` - Update team by nanoid * `POST /v2/teams` - Create a team * `GET /v2/teams/{team_nanoid}/member/{user_nanoid}/settings` - Get member settings * `PUT /v2/teams/{team_nanoid}/member/{user_nanoid}/settings` - Update member settings ## Authentication Migration ### B2B Authentication (New) **Using the Rise SDK (Recommended):** ```javascript theme={null} const { RiseApiClient } = require('@riseworks/sdk'); // Initialize SDK with Rise ID and private key const client = new RiseApiClient({ environment: 'prod', riseIdAuth: { riseId: process.env.RISE_ID, privateKey: process.env.PRIVATE_KEY } }); // SDK automatically handles SIWE authentication and JWT management const user = await client.me.get(); console.log('Authenticated as:', user.data.name); ``` **Alternative: Manual Authentication (Advanced):** ```javascript theme={null} // Manual SIWE authentication flow (for advanced use cases) class RiseAuth { constructor(baseUrl) { this.baseUrl = baseUrl; } async authenticate(walletAddress, privateKey, riseId) { // Step 1: Get SIWE message const siweResponse = await fetch( `${this.baseUrl}/v2/auth/siwe?wallet=${walletAddress}&riseid=${riseId}` ); const { siwe } = siweResponse.data.data; // Step 2: Sign with wallet const wallet = new ethers.Wallet(privateKey); const signature = await wallet.signMessage(siwe); // Extract nonce from SIWE message const nonceMatch = siwe.match(/Nonce: (.+)/); const nonce = nonceMatch ? nonceMatch[1] : ''; // Step 3: Verify and get JWT const verifyResponse = await fetch(`${this.baseUrl}/v2/auth/verify`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: siwe, sig: signature, nonce }) }); const { jwt } = verifyResponse.data.data; return jwt; } } // Use JWT in requests (same as V1) const apiResponse = await fetch(`${this.baseUrl}/v2/teams`, { headers: { 'Authorization': `Bearer ${jwt}` } }); ``` ## Data Format Changes ### Response Format **B2B Response:** ```json theme={null} { "success": true, "data": { "teams": [ { "nanoid": "te-abc123def456", "name": "Engineering Team", "created_at": "2024-01-01T00:00:00Z" } ] } } ``` ### Error Format **B2B Error:** ```json theme={null} { "success": false, "data": "Team te-abc123def456 not found" } ``` ## ID System Changes ### V1: Mixed ID System * Teams: `123`, `456`, `789` (numeric IDs) * Users: `talentId` (numeric IDs) * RiseID: `rise_id` (string format) ### B2B: Nanoid System * Teams: `te-abc123def456` * Users: `us-ghi789jkl012` * Companies: `co-def456ghi789` * Payments: `pa-pay123def456` ## Migration Checklist ### Phase 1: Preparation * [ ] **Review new authentication flow** - Update SIWE authentication paths * [ ] **Update base URL** - Change to new B2B API URLs (see [Environments](/environments)) * [ ] **Update endpoint paths** - Change from `/v1/` to `/v2/` * [ ] **Test authentication** - Verify updated SIWE flow works ### Phase 2: Core Migration * [ ] **Update team endpoints** - Replace numeric IDs with nanoids * [ ] **Update user endpoints** - Replace numeric IDs with nanoids * [ ] **Update balance endpoints** - Use new query parameter format * [ ] **Update response handling** - Handle new `{ success, data }` format ### Phase 3: Advanced Features * [ ] **Implement EIP-712 signing** - For payments and team management * [ ] **Update error handling** - Handle new error format * [ ] **Test all endpoints** - Verify functionality works as expected * [ ] **Update documentation** - Update internal API documentation ## Code Migration Examples ### Team Member Retrieval **V1 Code:** ```javascript theme={null} const getTeamMembers = async (teamId) => { const response = await fetch(`${this.baseUrl}/v1/teams/${teamId}/talent`, { headers: { 'Authorization': `Bearer ${jwt}` } }); const members = await response.json(); return members; // Returns array of members }; ``` **B2B Code (Using SDK):** ```javascript theme={null} const { RiseApiClient } = require('@riseworks/sdk'); const client = new RiseApiClient({ environment: 'prod', riseIdAuth: { riseId: process.env.RISE_ID, privateKey: process.env.PRIVATE_KEY } }); const getTeamMembers = async (teamNanoid) => { const response = await client.teams.getUsers({ team_nanoid: teamNanoid }); return response.data.users; // Returns array of users }; ``` **B2B Code (Manual HTTP):** ```javascript theme={null} const getTeamMembers = async (teamNanoid) => { const response = await fetch(`${this.baseUrl}/v2/teams/${teamNanoid}/users`, { headers: { 'Authorization': `Bearer ${jwt}` } }); const result = await response.json(); return result.data.users; // Returns array of users }; ``` ### Payment Processing **V1 Code:** ```javascript theme={null} const payV1 = async (teamId, recipients, token) => { const response = await fetch(`${this.baseUrl}/v1/payments/pay`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ team_id: teamId, recipients: recipients.map(r => ({ user_id: r.userId, amount: r.amount, currency: r.currency, description: r.description })) }) }); return await response.json(); }; ``` **B2B Code (Using SDK - Automatic):** ```javascript theme={null} const { RiseApiClient } = require('@riseworks/sdk'); const client = new RiseApiClient({ environment: 'prod', riseIdAuth: { riseId: process.env.RISE_ID, privateKey: process.env.PRIVATE_KEY // Required for automatic signing } }); const payB2B = async (teamNanoid, recipients) => { // SDK handles the entire payment flow automatically const payment = await client.payments.sendPayment({ from: teamNanoid, to: recipients, pay_now: true, // true = pay immediately, false = intent (pay later) network: 'arbitrum' }); return payment.data; }; // Usage example const recipients = [ { to: 'us-jRxg2LRL54DJ', amount_cents: 300, currency_symbol: 'USD', invoice_description: 'Papa Sent you', }, { to: 'us-d6JHBF2kuZjE', amount_cents: 700, currency_symbol: 'USD', invoice_description: 'Papa Sent you', }, ]; const payment = await payB2B('te-bXy7gjb_Iga-', recipients); console.log('Payment executed:', payment); ``` **B2B Code (Using SDK - Manual):** ```javascript theme={null} const { RiseApiClient } = require('@riseworks/sdk'); const client = new RiseApiClient({ environment: 'prod', jwtToken: process.env.JWT_TOKEN // JWT token is sufficient for manual flow }); const payB2BManual = async (teamNanoid, recipients, walletAddress, privateKey) => { // Step 1: Create payment draft const createResponse = await client.payments.getPaymentTypedData({ from: teamNanoid, to: recipients, pay_now: true, network: 'arbitrum' }); const { typed_data } = createResponse.data; console.log('Payment draft created:', typed_data); // Step 2: Sign typed data const { ethers } = require('ethers'); const wallet = new ethers.Wallet(privateKey); const signature = await wallet.signTypedData( typed_data.domain, typed_data.types, typed_data.typed_data ); // Step 3: Execute payment const executeResponse = await client.payments.executePaymentWithSignedData({ signer: walletAddress, from: teamNanoid, to: recipients, pay_now: true, typed_data: typed_data.typed_data, signature: signature }); console.log('Payment executed:', executeResponse.data); return executeResponse.data; }; ``` **B2B Code (Manual HTTP):** ```javascript theme={null} import { ethers } from 'ethers'; async function createPayment(teamNanoid, recipients, walletAddress, privateKey, payNow = true) { // Step 1: Create payment draft const createResponse = await fetch(`${this.baseUrl}/v2/payments`, { method: 'POST', headers: { 'Authorization': `Bearer ${jwt}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ from: teamNanoid, to: recipients, pay_now: payNow, // true = pay immediately, false = intent (pay later) network: 'arbitrum' }) }); if (!createResponse.ok) { const errorText = await createResponse.text(); throw new Error(`Failed to create payment draft: ${createResponse.status} - ${errorText}`); } const { data: typedData } = await createResponse.json(); console.log('Payment draft created:', typedData); // Step 2: Sign typed data const wallet = new ethers.Wallet(privateKey); const signature = await wallet.signTypedData( typedData.domain, typedData.types, typedData.typed_data ); // Step 3: Execute payment const executeResponse = await fetch(`${this.baseUrl}/v2/payments`, { method: 'PUT', headers: { 'Authorization': `Bearer ${jwt}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ signer: walletAddress, from: teamNanoid, to: recipients, pay_now: payNow, typed_data: typedData.typed_data, signature: signature }) }); const response = await executeResponse.json(); console.log('Payment executed:', response.data); return response.data; } // Usage example const to = [ { to: 'us-jRxg2LRL54DJ', amount_cents: 300, currency_symbol: 'USD', invoice_description: 'Papa Sent you', }, { to: 'us-d6JHBF2kuZjE', amount_cents: 700, currency_symbol: 'USD', invoice_description: 'Papa Sent you', }, ]; const payment = await createPayment( 'te-bXy7gjb_Iga-', to, '', '', true // true = pay immediately, false = pay intent (pay later) ); ``` Use `pay_now: true` to create and execute the payment immediately. With `pay_now: false` the payment is created as an intent and is not processed straight away — it must be approved by a user with payment execution permission before it is sent. ### Balance Retrieval **B2B Code (Using SDK):** ```javascript theme={null} const { RiseApiClient } = require('@riseworks/sdk'); const client = new RiseApiClient({ environment: 'prod', riseIdAuth: { riseId: process.env.RISE_ID, privateKey: process.env.PRIVATE_KEY } }); const getBalance = async (nanoid) => { const response = await client.entityBalance.get({ nanoid: nanoid }); return response.data; // Returns balance with account address and currency breakdown }; ``` **B2B Code (Manual HTTP):** ```javascript theme={null} const getBalance = async (nanoid) => { const response = await fetch(`${this.baseUrl}/v2/balance?nanoid=${nanoid}`, { headers: { 'Authorization': `Bearer ${jwt}` } }); const result = await response.json(); return result.data; // Returns balance with account address and currency breakdown }; ``` ## Common Migration Issues ### Issue 1: Authentication Errors **Problem:** Getting 401 errors after migration **Solution:** * Update SIWE endpoint paths from `/v1/api/siwe` to `/v2/auth/siwe` and `/v2/auth/verify` * Check that JWT tokens are included as `Authorization: Bearer ` headers * Verify JWT hasn't expired (24-hour lifetime) ### Issue 2: ID Format Errors **Problem:** Getting 404 errors for resources that should exist **Solution:** * Replace numeric IDs with nanoids * Update all endpoint URLs to use nanoid format * Check that you're using the correct nanoid type (team, user, company) ### Issue 3: Response Format Errors **Problem:** Code expecting old response format **Solution:** * Update response handling to use `response.data.data` instead of direct properties * Handle the new `{ success, data }` wrapper format * Update error handling to use `response.data` for error messages ### Issue 4: Missing EIP-712 Signing **Problem:** Getting errors about missing signatures **Solution:** * Implement EIP-712 typed data signing for operations that require it * Use the two-step process (create + execute) for payments and team management * Ensure wallet is connected and has proper permissions ## Support During Migration Complete B2B API documentation Interactive API reference Get migration assistance Check API status **Need Help?** Our support team is available to help with your migration. Contact us at [Hello@Riseworks.io](mailto:Hello@Riseworks.io) for personalized assistance. **Important**: The V1 API will be deprecated in the future. We recommend completing your migration as soon as possible to ensure continued access to Rise services. # Payment Integration Guide Source: https://docs.riseworks.io/guides/payment-integration Complete guide to integrating payments with Rise B2B API using the SDK Rise provides a comprehensive payment system that enables batch payments with blockchain security. This guide covers the complete payment flow using the Rise SDK for seamless integration. ## Payment Flow Overview Set up the Rise SDK with authentication Use SDK to create payment drafts with typed data SDK automatically handles signing and execution Track payment status and confirmations ## Payment Types ### Batch Payments Process multiple payments in a single transaction for efficiency and cost savings. ### Individual Payments Process single payments with immediate execution. ## SDK Installation First, install the Rise SDK: ```bash theme={null} npm install @riseworks/sdk # Or: yarn add @riseworks/sdk # Or: pnpm add @riseworks/sdk ``` ## Creating Payments ### Step 1: Initialize the SDK ```javascript theme={null} const { RiseApiClient } = require('@riseworks/sdk'); require('dotenv').config(); // Initialize SDK with private key for automatic signing const client = new RiseApiClient({ environment: 'prod', riseIdAuth: { riseId: process.env.RISE_ID, privateKey: process.env.PRIVATE_KEY // Required for automatic signing } }); // Alternative: Initialize with JWT token const jwtClient = new RiseApiClient({ environment: 'prod', jwtToken: process.env.JWT_TOKEN }); ``` ### Step 2: Create and Execute Payment The SDK provides two methods for creating payments: #### Method 1: Automatic Payment (Recommended) The SDK must be initialized with a private key for automatic signing: ```javascript theme={null} // Initialize SDK with private key for automatic signing const client = new RiseApiClient({ environment: 'prod', riseIdAuth: { riseId: process.env.RISE_ID, privateKey: process.env.PRIVATE_KEY // Required for automatic signing } }); // SDK handles everything automatically const payment = await client.payments.sendPayment({ from: 'te-abc123def456', to: [ { to: 'us-xyz789abc123', amount_cents: 10000, currency_symbol: 'USD', invoice_description: 'Salary payment for January 2024' }, { to: 'us-def456ghi789', amount_cents: 15000, currency_symbol: 'USD', invoice_description: 'Bonus payment' } ], pay_now: true, network: 'arbitrum' }); console.log('Payment executed:', payment.data); ``` #### Method 2: Manual Flow (Advanced) For manual control over the signing process: ```javascript theme={null} // Initialize SDK (private key not required for manual flow) const client = new RiseApiClient({ environment: 'prod', jwtToken: process.env.JWT_TOKEN // JWT token is sufficient for manual flow }); // Step 1: Get typed data for signing const typedDataResponse = await client.payments.getPaymentTypedData({ from: 'te-abc123def456', to: [ { to: 'us-xyz789abc123', amount_cents: 10000, currency_symbol: 'USD', invoice_description: 'Salary payment for January 2024' } ], pay_now: true, network: 'arbitrum' }); // Step 2: Sign the typed data (if you want custom signing logic) const { ethers } = require('ethers'); const wallet = new ethers.Wallet(process.env.PRIVATE_KEY); const signature = await wallet.signTypedData( typedDataResponse.data.domain, typedDataResponse.data.types, typedDataResponse.data.typed_data ); // Step 3: Execute with signature const executeResponse = await client.payments.executePaymentWithSignedData({ from: 'te-abc123def456', to: [ { to: 'us-xyz789abc123', amount_cents: 10000, currency_symbol: 'USD', invoice_description: 'Salary payment for January 2024' } ], pay_now: true, network: 'arbitrum', signer: wallet.address, typed_data: typedDataResponse.data.typed_data, signature: signature }); console.log('Payment executed:', executeResponse.data); ``` ## Complete Payment Integration Example Here's a complete example using the Rise SDK: ```javascript theme={null} const { RiseApiClient } = require('@riseworks/sdk'); require('dotenv').config(); class RisePaymentSystem { constructor() { this.client = new RiseApiClient({ environment: 'prod', riseIdAuth: { riseId: process.env.RISE_ID, privateKey: process.env.PRIVATE_KEY } }); } async createBatchPayment(teamNanoid, recipients, payNow = true) { try { console.log('Creating batch payment...'); const payment = await this.client.payments.sendPayment({ from: teamNanoid, to: recipients, pay_now: payNow, network: 'arbitrum' }); console.log('Payment executed successfully:', payment.data); return payment.data; } catch (error) { console.error('Payment failed:', error.message); throw error; } } async queryPayments(teamNanoid, options = {}) { try { const params = { team_nanoid: teamNanoid, state: options.state || 'all', query_type: options.queryType || 'payable', ...(options.startDate && { start_date: options.startDate }), ...(options.endDate && { end_date: options.endDate }), ...(options.recipient && { recipient: options.recipient }) }; const payments = await this.client.payments.get(params); return payments.data; } catch (error) { console.error('Failed to query payments:', error.message); throw error; } } async getPaymentHistory(teamNanoid, startDate, endDate) { return this.queryPayments(teamNanoid, { startDate, endDate, state: 'all' }); } } // Usage example const paymentSystem = new RisePaymentSystem(); const recipients = [ { to: 'us-jRxg2LRL54DJ', amount_cents: 300, currency_symbol: 'USD', invoice_description: 'Salary payment' }, { to: 'us-d6JHBF2kuZjE', amount_cents: 700, currency_symbol: 'USD', invoice_description: 'Bonus payment' } ]; // Create immediate payment const payment = await paymentSystem.createBatchPayment( 'te-bXy7gjb_Iga-', recipients, true // pay immediately ); // Query payment history const history = await paymentSystem.getPaymentHistory( 'te-bXy7gjb_Iga-', '2024-01-01', '2024-01-31' ); ``` ## Payment Status Tracking Monitor payment status using the SDK: ```javascript theme={null} class PaymentTracker { constructor() { this.client = new RiseApiClient({ environment: 'prod', riseIdAuth: { riseId: process.env.RISE_ID, privateKey: process.env.PRIVATE_KEY } }); } async queryPayments(teamNanoid, options = {}) { const params = { team_nanoid: teamNanoid, state: options.state || 'all', query_type: options.queryType || 'payable', ...(options.startDate && { start_date: options.startDate }), ...(options.endDate && { end_date: options.endDate }), ...(options.recipient && { recipient: options.recipient }) }; return await this.client.payments.get(params); } async waitForConfirmation(transactionHash, maxAttempts = 30) { const { ethers } = require('ethers'); const provider = new ethers.JsonRpcProvider('https://arb1.arbitrum.io/rpc'); for (let i = 0; i < maxAttempts; i++) { const receipt = await provider.getTransactionReceipt(transactionHash); if (receipt && receipt.confirmations > 0) { return { confirmed: true, blockNumber: receipt.blockNumber, gasUsed: receipt.gasUsed.toString(), status: receipt.status === 1 ? 'success' : 'failed' }; } // Wait 10 seconds before next check await new Promise(resolve => setTimeout(resolve, 10000)); } throw new Error('Transaction confirmation timeout'); } } ``` ## Payment Timing Options Rise supports two payment timing options: | Option | Description | Use Case | | ---------------- | ---------------------------------------------------- | --------------------------------------------------- | | `pay_now: true` | Payment executes immediately | Instant payroll, urgent payments | | `pay_now: false` | Payment intent — requires approval before it is sent | Payments that need sign-off from a payment executor | ## Error Handling The SDK provides comprehensive error handling: ```javascript theme={null} try { const payment = await client.payments.sendPayment({ from: 'te-abc123def456', to: recipients, pay_now: true, network: 'arbitrum' }); console.log('Payment successful:', payment.data); } catch (error) { console.error('Payment error:', error.message); // Handle specific error types based on message content if (error.message.includes('insufficient balance')) { console.error('Insufficient balance in the source account'); } else if (error.message.includes('invalid signature')) { console.error('Signature verification failed'); } else if (error.message.includes('expired')) { console.error('Payment deadline has expired'); } else if (error.message.includes('401')) { console.error('Authentication failed'); } else if (error.message.includes('403')) { console.error('Insufficient permissions'); } else { console.error('Payment failed:', error.message); } } ``` Common payment errors: | Error Message | Description | Solution | | ------------------------ | --------------------------------------- | --------------------------------------------- | | `insufficient balance` | Not enough funds in source account | Check entity balance before creating payments | | `invalid signature` | TypedData signature verification failed | SDK handles signing automatically | | `expired deadline` | Payment deadline has passed | Create a new payment with updated deadline | | `invalid recipient` | Recipient address is not valid | Verify recipient addresses and permissions | | `payment limit exceeded` | Payment amount exceeds limits | Check payment limits for the entity | ## Security Best Practices **Always verify payment details** before execution. Double-check amounts, recipients, and payment descriptions. * **SDK Authentication**: Use the SDK's built-in authentication and signing * **Environment Variables**: Store sensitive credentials securely * **Secondary Wallets**: Use dedicated wallets for API operations * **Amount Validation**: Validate payment amounts before submission * **Recipient Verification**: Ensure recipients have valid Rise accounts * **Error Handling**: Implement proper error handling for payment failures ## Testing Payments For testing, use the staging environment: ```javascript theme={null} // Staging configuration const stagingClient = new RiseApiClient({ environment: 'stg', riseIdAuth: { riseId: process.env.RISE_ID, privateKey: process.env.PRIVATE_KEY } }); // Test payment with small amounts const testRecipients = [ { to: 'us-test-user-nanoid', amount_cents: 100, currency_symbol: 'USD', invoice_description: 'Test payment' } ]; const testPayment = await stagingClient.payments.sendPayment({ from: 'te-test-team-nanoid', to: testRecipients, pay_now: true, network: 'arbitrum' }); ``` ## Real-World Integration Example ### Payroll System Integration ```javascript theme={null} class PayrollSystem { constructor() { this.client = new RiseApiClient({ environment: 'prod', riseIdAuth: { riseId: process.env.RISE_ID, privateKey: process.env.PRIVATE_KEY } }); } async processPayrollRun(payrollRun) { const recipients = payrollRun.employees.map(emp => ({ to: emp.user_nanoid, amount_cents: emp.amount_cents, currency_symbol: 'USD', invoice_description: `${emp.description} - ${payrollRun.pay_date}` })); try { const result = await this.client.payments.sendPayment({ from: payrollRun.team_nanoid, to: recipients, pay_now: true, network: 'arbitrum' }); // Store payroll run result await this.storePayrollResult(payrollRun.id, result); return result; } catch (error) { console.error(`Payroll run ${payrollRun.id} failed:`, error.message); throw error; } } async storePayrollResult(payrollId, result) { // Store in your database console.log(`Payroll ${payrollId} processed:`, result.data); } } // Usage const payrollSystem = new PayrollSystem(); const payrollRun = { id: 'payroll-2024-01', employees: [ { user_nanoid: 'us-emp1', amount_cents: 50000, description: 'January Salary' }, { user_nanoid: 'us-emp2', amount_cents: 45000, description: 'January Salary' } ], team_nanoid: 'te-company-team', pay_date: '2024-01-31' }; const result = await payrollSystem.processPayrollRun(payrollRun); ``` ## Next Steps Learn how to authenticate with Rise using the SDK Manage teams and permissions for payment operations Set up real-time payment notifications Comprehensive error handling and troubleshooting **Need help?** See the [Payments](/concepts/payments) page for detailed payment concepts or contact support at [Hello@Riseworks.io](mailto:Hello@Riseworks.io) # Team Management Guide Source: https://docs.riseworks.io/guides/team-management Manage teams and users with Rise B2B API using the SDK Learn how to manage teams and users with the Rise B2B API using the official SDK. The SDK provides methods for team updates, user management, and team invitations. ## Team Management Flow Set up the Rise SDK with authentication Retrieve teams associated with the current user Modify team information and settings Send invitations and manage team membership ## SDK Installation First, install the Rise SDK: ```bash theme={null} npm install @riseworks/sdk # Or: yarn add @riseworks/sdk # Or: pnpm add @riseworks/sdk ``` ## Getting Started ### Initialize the SDK ```javascript theme={null} const { RiseApiClient } = require('@riseworks/sdk'); require('dotenv').config(); // Initialize with Rise ID and private key (recommended) const client = new RiseApiClient({ environment: 'prod', riseIdAuth: { riseId: process.env.RISE_ID, privateKey: process.env.PRIVATE_KEY } }); // Alternative: Initialize with JWT token const jwtClient = new RiseApiClient({ environment: 'prod', jwtToken: process.env.JWT_TOKEN }); ``` ## Team Management Operations ### Get User's Teams Retrieve all teams associated with the current user: ```javascript theme={null} // Get all teams for the current user const teams = await client.user.getTeams(); console.log('User teams:', teams.data); // Example response structure // { // "success": true, // "data": { // "teams": [ // { // "nanoid": "te-abc123def456", // "name": "Engineering Team", // "description": "Core engineering team", // "member_count": 5, // "status": "active" // } // ] // } // } ``` ### Update Team Information Update team settings and information: ```javascript theme={null} // Update team information await client.team.update({ nanoid: 'te-abc123def456', name: 'Updated Engineering Team', description: 'Updated team description' }); console.log('Team updated successfully'); ``` ### Team Invitations The SDK provides different approaches for sending team invitations depending on the role: #### Regular Team Invitations For regular team members (employees, viewers), no signing is required: ```javascript theme={null} // Send invitation to regular team members (no signing required) const invitation = await client.invites.send({ nanoid: 'te-abc123def456', invites: [ { email: 'john.doe@example.com', role: 'team_employee', permissions: ['pay', 'view'] }, { email: 'jane.smith@example.com', role: 'team_viewer', permissions: ['view'] } ], role: 'team_employee', anonymous: false }); console.log('Invitations sent:', invitation.data); ``` **What the SDK does automatically:** 1. **Validates invitation data** and permissions 2. **Sends invitations** to the specified email addresses 3. **Handles authentication** and JWT token management 4. **Provides error handling** and validation #### Manager Invitations (Requires Signing) For manager-level invitations (team\_admin, finance\_admin), signing is required. The SDK must be initialized with a private key for automatic signing: ```javascript theme={null} // Initialize SDK with private key for automatic signing const client = new RiseApiClient({ environment: 'prod', riseIdAuth: { riseId: process.env.RISE_ID, privateKey: process.env.PRIVATE_KEY // Required for automatic signing } }); // Send manager invitation with automatic signing const managerInvite = await client.invites.sendManagerInvite({ emails: ['manager@example.com'], role: 'team_admin', nanoid: 'te-abc123def456' }); console.log('Manager invitation sent:', managerInvite.data); ``` **What the SDK does automatically:** 1. **Generates typed data** for EIP-712 signing 2. **Signs the typed data** using your private key 3. **Submits the signed invitation** to the Rise API 4. **Handles authentication** and JWT token management 5. **Provides error handling** and retry logic #### Manual Manager Invitations For more control over manager invitation signing, you can handle the signing manually: ```javascript theme={null} // Initialize SDK (private key not required for manual flow) const client = new RiseApiClient({ environment: 'prod', jwtToken: process.env.JWT_TOKEN // JWT token is sufficient for manual flow }); // Step 1: Get typed data for signing const typedDataResponse = await client.invites.getManagerInviteTypedData({ emails: ['manager@example.com'], role: 'team_admin', nanoid: 'te-abc123def456' }); console.log('Typed data received:', typedDataResponse.data); // Step 2: Sign the typed data (if you want custom signing logic) const { ethers } = require('ethers'); const wallet = new ethers.Wallet(process.env.PRIVATE_KEY); const signature = await wallet.signTypedData( typedDataResponse.data.typed_data.domain, typedDataResponse.data.typed_data.types, typedDataResponse.data.typed_data.typed_data ); // Step 3: Execute with signature const executeResponse = await client.invites.executeManagerInviteWithSignedData({ invites: typedDataResponse.data.invites, signer: wallet.address, typed_data: typedDataResponse.data.typed_data.typed_data, signature: signature }); console.log('Manager invitation executed:', executeResponse.data); ``` **What the manual flow provides:** 1. **Custom signing logic** - You control when and how to sign 2. **Typed data inspection** - You can examine the data before signing 3. **Signature verification** - You can verify the signature before submission 4. **Error handling control** - Custom error handling for each step 5. **Integration flexibility** - Easier integration with existing signing workflows ## Complete Team Management Example Here's a complete example using the Rise SDK: ```javascript theme={null} const { RiseApiClient } = require('@riseworks/sdk'); require('dotenv').config(); class RiseTeamManager { constructor() { this.client = new RiseApiClient({ environment: 'prod', riseIdAuth: { riseId: process.env.RISE_ID, privateKey: process.env.PRIVATE_KEY } }); } async getUserTeams() { try { const teams = await this.client.user.getTeams(); return teams.data; } catch (error) { console.error('Failed to get user teams:', error.message); throw error; } } async updateTeam(teamNanoid, updates) { try { await this.client.team.update({ nanoid: teamNanoid, ...updates }); console.log('Team updated successfully'); } catch (error) { console.error('Failed to update team:', error.message); throw error; } } async inviteTeamMembers(teamNanoid, members) { try { const invitation = await this.client.invites.send({ nanoid: teamNanoid, invites: members, role: 'team_employee', anonymous: false }); console.log('Team invitations sent:', invitation.data); return invitation.data; } catch (error) { console.error('Failed to send invitations:', error.message); throw error; } } async inviteManager(teamNanoid, email, role = 'team_admin') { try { const managerInvite = await this.client.invites.sendManagerInvite({ emails: [email], role: role, nanoid: teamNanoid }); console.log('Manager invitation sent:', managerInvite.data); return managerInvite.data; } catch (error) { console.error('Failed to send manager invitation:', error.message); throw error; } } async getTeamInvites(teamNanoid) { try { const invites = await this.client.invites.get({ nanoid: teamNanoid }); return invites.data; } catch (error) { console.error('Failed to get team invites:', error.message); throw error; } } } // Usage example const teamManager = new RiseTeamManager(); async function main() { try { // Get user's teams const teams = await teamManager.getUserTeams(); console.log('User teams:', teams); if (teams.teams && teams.teams.length > 0) { const firstTeam = teams.teams[0]; // Update team information await teamManager.updateTeam(firstTeam.nanoid, { name: 'Updated Engineering Team', description: 'Updated team description' }); // Invite new team members const newMembers = [ { email: 'developer1@example.com', role: 'team_employee', permissions: ['pay', 'view'] }, { email: 'developer2@example.com', role: 'team_employee', permissions: ['pay', 'view'] } ]; await teamManager.inviteTeamMembers(firstTeam.nanoid, newMembers); // Invite a manager await teamManager.inviteManager( firstTeam.nanoid, 'manager@example.com', 'team_admin' ); // Get team invitations const invites = await teamManager.getTeamInvites(firstTeam.nanoid); console.log('Team invitations:', invites); } } catch (error) { console.error('Team management error:', error.message); } } // Run the example if (require.main === module) { main(); } ``` ## Team Roles and Permissions | Role | Permissions | Description | | ----------------- | --------------------------------- | ---------------------------------------------- | | **Team Admin** | `pay`, `view`, `manage`, `invite` | Full control over team settings and members | | **Finance Admin** | `pay`, `view`, `finance` | Financial management capabilities | | **Team Employee** | `pay`, `view` | Standard team member with payment capabilities | | **Team Viewer** | `view` | Read-only access to team information | ## Error Handling The SDK provides comprehensive error handling: ```javascript theme={null} try { const teams = await client.user.getTeams(); console.log('Teams retrieved successfully:', teams.data); } catch (error) { console.error('Team management error:', error.message); // Handle specific error types based on message content if (error.message.includes('401')) { console.error('Authentication failed'); } else if (error.message.includes('403')) { console.error('Insufficient permissions for this operation'); } else if (error.message.includes('404')) { console.error('Team not found'); } else if (error.message.includes('invalid')) { console.error('Invalid team data provided'); } else { console.error('Team operation failed:', error.message); } } ``` Common team management errors: | Error Message | Description | Solution | | ------------------- | ------------------------- | --------------------------------------------- | | `401` | Authentication failed | Check your credentials and JWT token | | `403` | Insufficient permissions | Verify user has required role and permissions | | `404` | Team not found | Check team nanoid is correct | | `invalid team data` | Invalid team information | Verify team data format and requirements | | `invitation failed` | Failed to send invitation | Check email format and team permissions | ## Best Practices **Always maintain at least one team admin** to prevent losing access to team management capabilities. ### Team Management Best Practices * **Role Hierarchy**: Use the principle of least privilege when assigning roles * **Regular Audits**: Periodically review team members and their permissions * **Documentation**: Keep records of team structure and permission changes * **Backup Admins**: Always have multiple admins for critical teams * **Permission Reviews**: Regularly review and update member permissions ### SDK Best Practices * **Error Handling**: Implement proper error handling for all team operations * **Authentication**: Use Rise ID authentication for automatic JWT management * **Environment**: Use staging environment for testing team operations * **Logging**: Log team management operations for audit purposes ## Testing Team Management For testing, use the staging environment: ```javascript theme={null} // Staging configuration for testing const stagingClient = new RiseApiClient({ environment: 'stg', riseIdAuth: { riseId: process.env.RISE_ID, privateKey: process.env.PRIVATE_KEY } }); // Test team operations const testTeams = await stagingClient.user.getTeams(); console.log('Test teams:', testTeams.data); ``` ## Next Steps Learn how to authenticate with Rise using the SDK Process payments for team members Advanced blockchain integration features Comprehensive error handling and troubleshooting **Need help?** See the [Teams](/concepts/teams) page for detailed team management concepts or contact support at [Hello@Riseworks.io](mailto:Hello@Riseworks.io) # Web3 Integration Source: https://docs.riseworks.io/guides/web3-integration Integrate wallets and typed data signing with Rise B2B API using the SDK Rise B2B API is designed for seamless integration with Web3 wallets and smart contract standards. This guide covers wallet integration and EIP-712 typed data signing for secure blockchain operations using the official Rise SDK. EIP-712 typed data signing for enhanced security Gas-optimized transactions with structured data Works with MetaMask, WalletConnect, and other wallets Human-readable messages in wallet interfaces ## SDK Installation First, install the Rise SDK: ```bash theme={null} npm install @riseworks/sdk # Or: yarn add @riseworks/sdk # Or: pnpm add @riseworks/sdk ``` ## Supported Wallets Rise B2B API supports all major Web3 wallets: Most popular Ethereum wallet Multi-wallet connection protocol Coinbase's Web3 wallet Beautiful mobile wallet Binance's mobile wallet Compatible with any EIP-1193 wallet ## Getting Started ### Initialize the SDK ```javascript theme={null} const { RiseApiClient } = require('@riseworks/sdk'); require('dotenv').config(); // Initialize with Rise ID and private key (recommended for Web3 operations) const client = new RiseApiClient({ environment: 'prod', riseIdAuth: { riseId: process.env.RISE_ID, privateKey: process.env.PRIVATE_KEY } }); // Alternative: Initialize with JWT token const jwtClient = new RiseApiClient({ environment: 'prod', jwtToken: process.env.JWT_TOKEN }); ``` ## EIP-712 Typed Data Signing Many Rise operations (payments, team management, invites) require **EIP-712 typed data signing** for enhanced security and gas efficiency. The SDK handles this automatically for most operations. ### What is EIP-712 Typed Data? EIP-712 is a standard for typed data signing that provides: * **Human-readable messages** in wallet interfaces * **Type safety** to prevent signature replay attacks * **Gas efficiency** compared to raw message signing * **Better security** through structured data validation ### Automatic Typed Data Signing The SDK automatically handles typed data signing for most operations: ```javascript theme={null} // Automatic payment with signing const payment = await client.payments.sendPayment({ from: 'te-abc123def456', to: [ { to: 'us-xyz789abc123', amount_cents: 10000, currency_symbol: 'USD', invoice_description: 'Salary payment for January 2024' } ], pay_now: true, network: 'arbitrum' }); console.log('Payment executed:', payment.data); ``` **What the SDK does automatically:** 1. **Generates typed data** for EIP-712 signing 2. **Signs the typed data** using your private key 3. **Submits the signed transaction** to the Rise API 4. **Handles authentication** and JWT token management 5. **Provides error handling** and retry logic ### Manual Typed Data Signing For more control over the signing process, you can handle the signing manually: ```javascript theme={null} // Initialize SDK (private key not required for manual flow) const client = new RiseApiClient({ environment: 'prod', jwtToken: process.env.JWT_TOKEN // JWT token is sufficient for manual flow }); // Step 1: Get typed data for signing const typedDataResponse = await client.payments.getPaymentTypedData({ from: 'te-abc123def456', to: [ { to: 'us-xyz789abc123', amount_cents: 10000, currency_symbol: 'USD', invoice_description: 'Salary payment for January 2024' } ], pay_now: true, network: 'arbitrum' }); console.log('Typed data received:', typedDataResponse.data); // Step 2: Sign the typed data (if you want custom signing logic) const { ethers } = require('ethers'); const wallet = new ethers.Wallet(process.env.PRIVATE_KEY); const signature = await wallet.signTypedData( typedDataResponse.data.domain, typedDataResponse.data.types, typedDataResponse.data.typed_data ); // Step 3: Execute with signature const executeResponse = await client.payments.executePaymentWithSignedData({ from: 'te-abc123def456', to: [ { to: 'us-xyz789abc123', amount_cents: 10000, currency_symbol: 'USD', invoice_description: 'Salary payment for January 2024' } ], pay_now: true, network: 'arbitrum', signer: wallet.address, typed_data: typedDataResponse.data.typed_data, signature: signature }); console.log('Payment executed:', executeResponse.data); ``` **What the manual flow provides:** 1. **Custom signing logic** - You control when and how to sign 2. **Typed data inspection** - You can examine the data before signing 3. **Signature verification** - You can verify the signature before submission 4. **Error handling control** - Custom error handling for each step 5. **Integration flexibility** - Easier integration with existing signing workflows ## When to Use Typed Data Signing Typed data signing is required for the following Rise operations: Creating and executing payments Adding/removing team members Sending team invitations Updating team settings Managing role permissions Processing withdrawals ## Complete Integration Example Here's a complete example showing how to integrate Web3 operations with Rise API using the SDK: ```javascript theme={null} const { RiseApiClient } = require('@riseworks/sdk'); require('dotenv').config(); class RiseWeb3Integration { constructor() { this.client = new RiseApiClient({ environment: 'prod', riseIdAuth: { riseId: process.env.RISE_ID, privateKey: process.env.PRIVATE_KEY } }); } // Automatic payment with signing async createPayment(teamNanoid, recipients, payNow = true) { try { const payment = await this.client.payments.sendPayment({ from: teamNanoid, to: recipients, pay_now: payNow, network: 'arbitrum' }); console.log('Payment executed:', payment.data); return payment.data; } catch (error) { console.error('Payment failed:', error.message); throw error; } } // Automatic manager invite with signing async createManagerInvite(teamNanoid, emails, role) { try { const managerInvite = await this.client.invites.sendManagerInvite({ emails: emails, role: role, nanoid: teamNanoid }); console.log('Manager invite sent:', managerInvite.data); return managerInvite.data; } catch (error) { console.error('Manager invite failed:', error.message); throw error; } } // Automatic withdrawal with signing async processWithdrawal(accountNanoid, amount, currency) { try { const withdrawal = await this.client.withdraw.sendWithdraw( { account_nanoid: accountNanoid }, { amount_cents: amount, currency_symbol: currency } ); console.log('Withdrawal processed:', withdrawal.data); return withdrawal.data; } catch (error) { console.error('Withdrawal failed:', error.message); throw error; } } // Manual payment with custom signing async createManualPayment(teamNanoid, recipients, payNow = true) { try { // Step 1: Get typed data const typedDataResponse = await this.client.payments.getPaymentTypedData({ from: teamNanoid, to: recipients, pay_now: payNow, network: 'arbitrum' }); console.log('Typed data received:', typedDataResponse.data); // Step 2: Sign the typed data const { ethers } = require('ethers'); const wallet = new ethers.Wallet(process.env.PRIVATE_KEY); const signature = await wallet.signTypedData( typedDataResponse.data.domain, typedDataResponse.data.types, typedDataResponse.data.typed_data ); // Step 3: Execute with signature const executeResponse = await this.client.payments.executePaymentWithSignedData({ from: teamNanoid, to: recipients, pay_now: payNow, network: 'arbitrum', signer: wallet.address, typed_data: typedDataResponse.data.typed_data, signature: signature }); console.log('Manual payment executed:', executeResponse.data); return executeResponse.data; } catch (error) { console.error('Manual payment failed:', error.message); throw error; } } // Generic typed data signing utility async signTypedData(typedData, privateKey) { const { ethers } = require('ethers'); const wallet = new ethers.Wallet(privateKey); const signature = await wallet.signTypedData( typedData.domain, typedData.types, typedData.typed_data ); return signature; } } // Usage example const web3Integration = new RiseWeb3Integration(); async function main() { try { // Example: Create automatic payment const recipients = [ { to: 'us-jRxg2LRL54DJ', amount_cents: 300, currency_symbol: 'USD', invoice_description: 'Salary payment' }, { to: 'us-d6JHBF2kuZjE', amount_cents: 700, currency_symbol: 'USD', invoice_description: 'Bonus payment' } ]; const payment = await web3Integration.createPayment( 'te-abc123def456', recipients, true // pay immediately ); // Example: Create manager invite const managerInvite = await web3Integration.createManagerInvite( 'te-abc123def456', ['admin@company.com'], 'team_admin' ); // Example: Process withdrawal const withdrawal = await web3Integration.processWithdrawal( 'ac-xyz789abc123', 5000, // $50.00 'USD' ); // Example: Manual payment with custom signing const manualPayment = await web3Integration.createManualPayment( 'te-abc123def456', recipients, true ); } catch (error) { console.error('Web3 integration error:', error.message); } } // Run the example if (require.main === module) { main(); } ``` ## Security Best Practices * Never store private keys in client-side code * Use environment variables for server-side operations * Consider hardware wallets for high-value transactions * Always verify signatures before submitting * Check contract addresses and chain IDs * Validate typed data structure * Use HTTPS for all API communications * Implement proper error handling * Log security events for auditing * Respect expiration times in typed data * Implement retry logic for failed transactions * Monitor transaction status ## Error Handling The SDK provides comprehensive error handling: ```javascript theme={null} try { const payment = await client.payments.sendPayment({ from: 'te-abc123def456', to: [ { to: 'us-xyz789abc123', amount_cents: 10000, currency_symbol: 'USD', invoice_description: 'Payment' } ], pay_now: true, network: 'arbitrum' }); console.log('Payment successful:', payment.data); } catch (error) { console.error('Web3 operation error:', error.message); // Handle specific error types based on message content if (error.message.includes('insufficient balance')) { console.error('Insufficient funds for payment'); } else if (error.message.includes('invalid signature')) { console.error('Signature verification failed'); } else if (error.message.includes('expired')) { console.error('Typed data has expired'); } else if (error.message.includes('wrong network')) { console.error('Incorrect network connected'); } else { console.error('Web3 operation failed:', error.message); } } ``` Common Web3 errors: | Error Message | Description | Solution | | ---------------------- | -------------------------------- | ------------------------------------ | | `insufficient balance` | Not enough funds for transaction | Ensure wallet has sufficient balance | | `invalid signature` | Signature verification failed | Ensure the correct wallet is signing | | `expired` | Typed data has expired | Request fresh typed data from API | | `wrong network` | Incorrect network connected | Switch to the correct network | | `invalid typed data` | Invalid typed data structure | Check typed data format | ## Testing Web3 Integration For testing, use the staging environment: ```javascript theme={null} // Staging configuration for testing const stagingClient = new RiseApiClient({ environment: 'stg', riseIdAuth: { riseId: process.env.RISE_ID, privateKey: process.env.PRIVATE_KEY } }); // Test Web3 operations const testPayment = await stagingClient.payments.sendPayment({ from: 'te-test123', to: [ { to: 'us-test456', amount_cents: 100, currency_symbol: 'USD', invoice_description: 'Test payment' } ], pay_now: true, network: 'arbitrum' }); console.log('Test payment executed:', testPayment.data); ``` ## Environment Configuration For the complete list of API URLs for each environment, see our [Environments](/concepts/environments) page. ```javascript theme={null} // Staging (Arbitrum Sepolia) const STAGING_CONFIG = { environment: 'stg', chainId: 421614, verifyingContract: '0x...' // Sepolia contract address }; // Production (Arbitrum mainnet) const PROD_CONFIG = { environment: 'prod', chainId: 42161, verifyingContract: '0x...' // Mainnet contract address }; ``` ```typescript theme={null} interface Web3Config { environment: 'stg' | 'prod'; chainId: number; verifyingContract: string; } const STAGING_CONFIG: Web3Config = { environment: 'stg', chainId: 421614, verifyingContract: '0x...' }; const PROD_CONFIG: Web3Config = { environment: 'prod', chainId: 42161, verifyingContract: '0x...' }; ``` ## Next Steps Learn how to authenticate with Rise using the SDK Complete payment flow with typed data Managing teams with Web3 User onboarding flow **Need help?** Check out our [Payment Integration Guide](/guides/payment-integration) for a complete example of typed data signing in action. # Rise B2B API Documentation Source: https://docs.riseworks.io/index Build powerful integrations with Rise's blockchain-based global payroll and team management platform Legacy (v1) users will be migrated to the new infrastructure soon. While your current integrations will initially remain compatible, v1 endpoints will eventually be deprecated. Please read the [**\[New Rise API Migration Guide\]**](/migrated-clients) to prepare your code and check the deprecation timeline. # Rise B2B API **Global Payroll for the Modern Workforce** Streamline payments to contractors and full-time employees worldwide with Rise's blockchain-first platform. Our B2B API enables seamless integration for payroll, team management, and compliance automation across 190+ countries. Sign-In with Ethereum (SIWE) for secure, blockchain-based authentication Process payments in 90+ local currencies and 100+ cryptocurrencies Manage teams, roles, and permissions with comprehensive RBAC EIP-712 typed data signing for secure blockchain operations ## Why Rise B2B API? All actions secured and executed on-chain with smart contracts on Arbitrum Automated identity verification and compliance across 190+ countries Pay in local currency, stablecoins, or cryptocurrencies Comprehensive RBAC system for secure team management Monitor entity balances across multiple currencies RESTful API with comprehensive documentation and examples ## Platform Statistics Local Currencies Supported Cryptocurrencies Supported Paid to Teams Globally Countries Supported ## Quick Start Begin your integration with Rise B2B API in minutes: Configure SIWE authentication to securely connect with Rise's platform Organize your workforce with teams and role-based permissions Execute payments in multiple currencies with EIP-712 signing Track entity balances and transaction history in real-time ## Core Features ### Authentication & Security * **Sign-In with Ethereum (SIWE)** for secure wallet-based authentication * **JWT token management** with 24-hour expiration * **Role-based access control** with granular permissions * **EIP-712 typed data signing** for secure blockchain operations ### Team Management * **Hierarchical organization** with companies and teams * **Comprehensive role system** (admin, finance, employee, viewer, contractor) * **Bulk invite system** for efficient onboarding * **Permission validation** for secure operations ### Payment Processing * **Multi-currency support** (90+ local currencies, 100+ cryptocurrencies) * **Batch payment processing** for efficiency * **Real-time transaction monitoring** * **Gas-optimized blockchain transactions** ### Balance Management * **Real-time balance tracking** across all supported currencies * **Entity-based balance system** using nanoid identifiers * **Secure balance queries** with proper authorization * **Multi-currency balance aggregation** ## Getting Started Begin your integration in 5 minutes with our step-by-step guide Learn about SIWE authentication and JWT token management Understand RiseID, teams, roles, and entity management Explore the complete API specification with examples ## Real-Time Notifications Stay updated with real-time events and notifications through Rise's webhook system: Understand how webhooks work and their benefits for real-time integration Set up your first webhook endpoint in minutes with our step-by-step guide Learn about webhook security, signature validation, and best practices Explore all available webhook events: payments, deposits, invites, and security Manage webhook endpoints, configure delivery settings, and monitor performance Test webhook delivery, debug issues, and implement proper error handling ## Support & Resources Complete authentication and integration guides Full API specification with examples Troubleshooting and error resolution Get help from our technical support team Real-time notifications and event handling Access the Rise dashboard for team management **Ready to begin your integration?** Start with our [Quickstart Guide](/quickstart) to configure your first integration with Rise B2B API. # Migrated Clients Source: https://docs.riseworks.io/migrated-clients **TL;DR:** Rise is launching a new API. Your existing integration will keep working after migration, but **database IDs are changing from integers to strings**. This affects **webhook responses**, the **Talents API**, and the **Teams API**. Only **new entities created on the new platform** will have string IDs — existing entities keep their integer IDs. Update your code to accept both `string` and `number` IDs before migration to avoid breakage. ### API Migration Guide Welcome. This page contains essential information for developers currently integrated with the Rise API. We are launching a brand-new version of our API and have designed a transition plan to ensure your current integration continues to work smoothly. The sections below outline the steps you need to take to prepare for the new Rise API. ​ #### 1. Before the Migration Before your account is migrated to the new Rise platform, please review the critical requirement below to ensure your current integration remains flawless. * **Identifier (ID) Format Change:** In the current API, all database identifiers are integers. In the new API, identifiers will be strings. You must update your system to handle *both* string and integer IDs immediately. After the initial backend migration, your existing code will continue to function, but any newly created entities (such as teams or payees) will be assigned string IDs. If your code strictly expects integers, it will break. ​ * Id update example: ```typescript theme={null} // Define the expected payload where 'id' can be either format interface RisePayload { id: string | number; // Old API returns number, New API returns string name: string; } // Function to process the incoming data function handleRiseEntity(payload: RisePayload) { // Safely cast the ID to a string regardless of its original type const normalizedId: string = String(payload.id); // From this point on, your system should rely solely on 'normalizedId' console.log(`Processing entity with ID: ${normalizedId}`); // Example: saveToDatabase(normalizedId, payload.name); } // 1. Simulating the old API behavior (Integer) const oldData = { id: 84729, name: "Legacy Team" }; handleRiseEntity(oldData); // Output: Processing entity with ID: 84729 // 2. Simulating the new API behavior (String) const newData = { id: "a1b2c3d4-5678", name: "New Team" }; handleRiseEntity(newData); // Output: Processing entity with ID: a1b2c3d4-5678 ``` #### 2. Immediately After the Migration Once the initial migration is complete, your system will be powered by the new Rise infrastructure, but you will still interact with it using the exact same interfaces, payloads, webhooks, and endpoints as before. The only immediate change you will notice is the string ID format mentioned above. # Quickstart Source: https://docs.riseworks.io/quickstart Get started with Rise B2B API in minutes Welcome to Rise B2B API! This guide will help you integrate with our blockchain-based global payroll platform in minutes. Configure authentication and execute your first API call in under 5 minutes Built on blockchain with SIWE authentication and EIP-712 signing Support for 190+ countries and 90+ local currencies Process payments in 100+ cryptocurrencies and stablecoins ## Prerequisites **Request API access first, and do it per environment.** The B2B API is off by default. Contact Rise support to enable your company for the environment you'll call, and note that staging and production are enabled separately, so a request for one does not cover the other. Reach support through your dedicated Slack channel, the in-app chat, or [hello@riseworks.io](mailto:hello@riseworks.io). The full checklist, including authorizing your wallet, is in [Getting API Access](/authentication/api-access). Before beginning your integration, ensure you have: * **A Rise account** with a RiseID (get one at [app.riseworks.io](https://app.riseworks.io/)) * **Node.js** installed (version 16 or higher) * **Basic knowledge** of JavaScript/TypeScript **New to blockchain?** No concerns! You can use a private key from your wallet to authenticate with Rise, or use a JWT token. A private key functions as a digital signature that verifies wallet ownership, which connects to your Rise account. Learn more about [how wallets work](/concepts/wallets) and [authentication methods](/authentication). ## Step 1: Configure Your Environment First, create a new project and install the Rise SDK: ```bash theme={null} mkdir rise-b2b-integration cd rise-b2b-integration npm init -y npm install @riseworks/sdk dotenv # Or: yarn add @riseworks/sdk dotenv # Or: pnpm add @riseworks/sdk dotenv ``` Create a `.env` file for your configuration: ```bash theme={null} # Environment (optional - defaults to 'prod') RISE_ENVIRONMENT=prod # or 'stg' for staging # Your personal (user) RiseID — a 42-char 0x address, not a co-/te- nanoid RISE_ID=0xYourUserRiseIdHere WALLET_PRIVATE_KEY=0xYourPrivateKeyHere # Alternative: JWT token (if you have one) # JWT_TOKEN=your_jwt_token_here ``` **Need help with authentication?** Review our [Private Keys Guide](/authentication/private-keys) for step-by-step instructions on how to retrieve your private key from MetaMask, hardware wallets, or generate a new one. You can also learn about [how wallets work](/concepts/wallets) and [authentication methods](/authentication). **Security Note**: Never commit your `.env` file to version control. Your private key gives full access to your wallet. Store it securely and use environment variables in production. ## Step 2: Initialize the Rise SDK Create your first integration script: ```javascript theme={null} // quickstart.js require('dotenv').config(); const { RiseApiClient } = require('@riseworks/sdk'); // Initialize the Rise API client with your credentials const client = new RiseApiClient({ environment: process.env.RISE_ENVIRONMENT || 'prod', riseIdAuth: { riseId: process.env.RISE_ID, privateKey: process.env.WALLET_PRIVATE_KEY } }); // Alternative: Initialize with JWT token // const client = new RiseApiClient({ // environment: process.env.RISE_ENVIRONMENT || 'prod', // jwtToken: process.env.JWT_TOKEN // }); async function main() { try { console.log('🚀 Starting Rise B2B API integration...\n'); // The SDK automatically handles authentication console.log('Authenticating with Rise...'); // Retrieve current user information const user = await client.me.get(); console.log('User authenticated:', user.data.name); console.log('User Rise ID:', user.data.riseid); // Retrieve user's companies console.log('\n📊 Fetching user companies...'); const companies = await client.user.getCompanies(); console.log('Companies:', companies.data.companies.length); // Retrieve user's teams console.log('\n👥 Fetching user teams...'); const teams = await client.user.getTeams(); console.log('Teams:', teams.data.teams.length); // Get entity balance (replace with actual nanoid) if (companies.data.companies.length > 0) { const companyNanoid = companies.data.companies[0].nanoid; console.log(`\n💰 Fetching balance for company: ${companyNanoid}`); const balance = await client.entityBalance.get({ nanoid: companyNanoid }); console.log('Balance:', balance.data); } console.log('\n🎉 Integration test completed successfully!'); } catch (error) { console.error('\n💥 Integration test failed:', error.message); process.exit(1); } } main(); ``` ## Step 3: Execute Your Integration Execute your integration: ```bash theme={null} node quickstart.js ``` ## Step 4: Advanced SDK Usage Here are additional examples using the Rise SDK: ```javascript theme={null} // advanced-examples.js require('dotenv').config(); const { RiseApiClient } = require('@riseworks/sdk'); const client = new RiseApiClient({ riseIdAuth: { riseId: process.env.RISE_ID, privateKey: process.env.WALLET_PRIVATE_KEY } }); async function advancedExamples() { try { // 1. Execute a payment using the SDK console.log('💳 Processing payment...'); const payment = await client.payments.sendPayment({ team_nanoid: 'your_team_nanoid', payments: [ { recipient_nanoid: 'recipient_nanoid', amount: '100.00', currency: 'USD', description: 'Test payment via SDK' } ] // SDK automatically handles secure signing }); console.log('Payment sent:', payment.data); // 2. Execute team invites console.log('\n📧 Sending team invites...'); const invites = await client.invites.send({ team_nanoid: 'your_team_nanoid', invites: [ { email: 'team@example.com', role: 'employee' } ] }); console.log('Invites sent:', invites.data); // 3. Execute manager invite with automatic signing console.log('\n👨‍💼 Sending manager invite...'); const managerInvite = await client.invites.sendManagerInvite({ team_nanoid: 'your_team_nanoid', invites: [ { email: 'manager@example.com', role: 'finance' } ] // SDK automatically handles secure signing }); console.log('Manager invite sent:', managerInvite.data); // 4. Execute withdrawal console.log('\n💸 Processing withdrawal...'); const withdrawal = await client.withdraw.sendWithdraw( { account_nanoid: 'your_account_nanoid' }, { amount: '50.00', currency: 'USD', destination: 'bank_account' } // SDK automatically handles secure signing ); console.log('Withdrawal processed:', withdrawal.data); } catch (error) { console.error('Error in advanced examples:', error.message); } } advancedExamples(); ``` ## Step 5: SDK Configuration The SDK automatically uses production environment by default, but you can specify staging for testing: ```javascript theme={null} // Production (default) const prodClient = new RiseApiClient({ riseIdAuth: { riseId: process.env.RISE_ID, privateKey: process.env.WALLET_PRIVATE_KEY } }); // Staging (for testing) const stagingClient = new RiseApiClient({ environment: 'stg', riseIdAuth: { riseId: process.env.RISE_ID, privateKey: process.env.WALLET_PRIVATE_KEY } }); // With JWT token (if you have one) const jwtClient = new RiseApiClient({ jwtToken: process.env.JWT_TOKEN }); // Staging with JWT token const stagingJwtClient = new RiseApiClient({ environment: 'stg', jwtToken: process.env.JWT_TOKEN }); ``` ## Step 6: Error Handling The SDK provides comprehensive error handling: ```javascript theme={null} // error-handling.js const { RiseApiClient } = require('@riseworks/sdk'); const client = new RiseApiClient({ environment: process.env.RISE_ENVIRONMENT || 'prod', riseIdAuth: { riseId: process.env.RISE_ID, privateKey: process.env.WALLET_PRIVATE_KEY } }); async function handleErrors() { try { // SDK automatically handles authentication errors const user = await client.me.get(); console.log('User:', user.data); } catch (error) { if (error.message.includes('401')) { console.error('Authentication failed - verify your Rise ID and private key'); } else if (error.message.includes('403')) { console.error('Permission denied - verify your role permissions'); } else if (error.message.includes('404')) { console.error('Resource not found - verify the nanoid'); } else { console.error('API Error:', error.message); } } } handleErrors(); ``` ## Next Steps Now that you have basic SDK integration configured, explore these features: Install and configure the Rise SDK Learn about RiseID, teams, roles, and entity management Complete API documentation with all endpoints Set up webhook endpoints for real-time notifications Learn how to process payments securely Learn about different authentication methods Create teams and manage members with role-based access Comprehensive error handling and debugging guide **Production Ready?** The SDK automatically uses production environment by default. For testing, use `environment: 'stg'` to connect to staging. Implement comprehensive error handling before going live. The SDK automatically handles authentication and secure signing for you. # SDK Configuration Source: https://docs.riseworks.io/sdk/configuration Configure the Rise SDK for your environment and use case Proper SDK configuration is essential for secure and efficient API integration. ## Configuration Options The Rise SDK provides flexible configuration options to adapt to your specific needs. You can configure authentication methods, network settings, and environment-specific behavior. ### Core Configuration Parameters The SDK supports the following configuration options: ```typescript theme={null} import { RiseApiClient } from '@riseworks/sdk'; const client = new RiseApiClient({ environment: 'prod', // 'dev' | 'stg' | 'prod' (default: 'prod') baseUrl: 'https://custom-api.com', // Custom base URL (overrides environment) jwtToken: 'your-jwt-token', // JWT authentication riseIdAuth: { // SIWE authentication riseId: '0x...', privateKey: '0x...' }, timeout: 30000 // Request timeout in ms (default: 30000) }); ``` ### Configuration Priority The SDK follows a specific priority order for configuration: 1. **Custom baseUrl** (if provided, overrides environment) 2. **Environment setting** (determines API endpoint) 3. **Authentication method** (JWT token or SIWE credentials) 4. **Network settings** (timeout, headers) ## Authentication Configuration The Rise SDK supports two authentication methods, each with different use cases and security implications. ### JWT Authentication JWT (JSON Web Token) authentication is the simplest method for API access. Use this when you already have a valid JWT token from your authentication system. **Best for:** Quick integration, existing JWT tokens, read-only operations ```typescript theme={null} // Simple JWT configuration const client = new RiseApiClient({ environment: 'prod', jwtToken: process.env.JWT_TOKEN }); ``` **Note:** JWT tokens are obtained through SIWE authentication or your existing authentication system. ### SIWE Authentication SIWE (Sign-In with Ethereum) authentication provides the most comprehensive access. The SDK automatically handles JWT token generation and renewal. **Best for:** Full API access, sensitive operations, automatic token management ```typescript theme={null} // SIWE with Rise ID and private key const client = new RiseApiClient({ environment: 'prod', riseIdAuth: { riseId: process.env.RISE_ID, privateKey: process.env.PRIVATE_KEY } }); ``` **Security Note:** Always use environment variables for sensitive credentials and consider using secondary wallets for API operations. ## Network Configuration Network configuration allows you to customize how the SDK communicates with the Rise API, including timeout settings and custom headers. ### Request Timeout The timeout setting controls how long the SDK waits for API responses before considering the request failed. This is crucial for handling slow network connections or API delays. **Default:** 30 seconds (30000ms) ```typescript theme={null} // Custom timeout (default: 30 seconds) const client = new RiseApiClient({ environment: 'prod', jwtToken: process.env.JWT_TOKEN, timeout: 60000 // 60 seconds }); ``` **When to adjust timeout:** * **Increase timeout:** Slow network connections, large data transfers * **Decrease timeout:** Real-time applications, fail-fast scenarios ## Webhook Validator Configuration The Rise SDK includes a built-in webhook validator that ensures the security and authenticity of incoming webhook events. This is essential for protecting your application from malicious requests. ### Basic Webhook Validator Setup The webhook validator uses HMAC-SHA256 signatures to verify that webhooks are genuinely from Rise. This prevents replay attacks and ensures data integrity. ```typescript theme={null} import { WebhookValidator } from '@riseworks/sdk'; // Initialize with webhook secret const validator = new WebhookValidator(process.env.WEBHOOK_SECRET); // Express.js webhook endpoint app.post('/webhooks/rise', (req, res) => { try { // Validate webhook signature const event = validator.validateEvent( req.body, req.headers['rise-signature'] ); console.log('Valid webhook received:', event.event_type); res.status(200).json({ received: true }); } catch (error) { console.error('Webhook validation failed:', error.message); res.status(400).json({ error: 'Invalid signature' }); } }); ``` **Security Features:** * **HMAC-SHA256 signatures** for cryptographic verification * **Timestamp validation** to prevent replay attacks * **Automatic signature parsing** from Rise headers ### Advanced Webhook Validator Configuration For production applications, you may want more control over webhook validation behavior. The SDK provides advanced configuration options for custom tolerance settings and non-throwing validation. ```typescript theme={null} import { WebhookValidator } from '@riseworks/sdk'; // Initialize with custom tolerance (default: 600 seconds = 10 minutes) const validator = new WebhookValidator(process.env.WEBHOOK_SECRET, { tolerance: 300 // 5 minutes tolerance }); // Safe validation (non-throwing) app.post('/webhooks/rise', (req, res) => { const result = validator.validateEventSafe( req.body, req.headers['rise-signature'] ); if (result.isValid && result.event) { console.log('Valid event:', result.event.event_type); // Process event res.status(200).json({ received: true }); } else { console.error('Validation failed:', result.error); res.status(400).json({ error: result.error }); } }); ``` **Advanced Features:** * **Custom tolerance:** Adjust time window for timestamp validation * **Safe validation:** Non-throwing validation with detailed error information * **Event typing:** TypeScript support for webhook events ### Webhook Signature Generation The SDK provides signature generation capabilities for testing and custom validation scenarios. This is useful for creating test webhooks or implementing custom validation logic. ```typescript theme={null} // Generate signature for testing const timestamp = Math.floor(Date.now() / 1000); const payload = JSON.stringify({ test: 'data' }); const signature = validator.generateSignature(timestamp, payload); console.log('Generated signature:', signature); ``` **Use Cases:** * **Testing:** Generate valid signatures for webhook testing * **Custom validation:** Implement custom validation logic * **Debugging:** Verify signature generation matches Rise's format ## Next Steps Copy Claude-style skills from `@riseworks/sdk` into `.claude/skills/`. Learn about JWT and SIWE authentication methods and how to implement them in your application. Set up webhook endpoints, implement validation, and handle real-time events from Rise. Explore all available API endpoints, request/response formats, and integration examples. Learn how to handle errors, implement retry logic, and debug common integration issues. # SDK Installation Source: https://docs.riseworks.io/sdk/installation Install and set up the Rise SDK for your project The Rise SDK provides a comprehensive TypeScript/JavaScript client for integrating with Rise B2B API. ## Installation ### NPM ```bash theme={null} npm install @riseworks/sdk ``` ### Yarn ```bash theme={null} yarn add @riseworks/sdk ``` ### PNPM ```bash theme={null} pnpm add @riseworks/sdk ``` ## Quick Start ```typescript theme={null} import { RiseApiClient } from '@riseworks/sdk'; // Initialize with JWT authentication const client = new RiseApiClient({ environment: 'prod', jwtToken: process.env.JWT_TOKEN }); // Make your first API call const user = await client.me.get(); console.log('User:', user); ``` ### Common Issues * Check peer dependencies * Verify TypeScript configuration * Ensure correct import syntax * Verify environment variables * Check token expiration * Validate private key format * Check internet connection * Verify API endpoints * Review firewall settings * Update TypeScript version * Check type definitions * Verify SDK version ### Getting Help * **Documentation**: Check this documentation for detailed guides * **GitHub Issues**: Report bugs and request features * **Support**: Contact support for enterprise assistance * **Community**: Join our Discord for community help ## Next Steps After installation, proceed to: * **[Configuration](./configuration)** - Configure the SDK for your environment * **[Skills](./skills/ai-coding-skills)** - Copy Claude-style skills from `@riseworks/sdk` into `.claude/skills/` * **[Authentication Guide](/authentication)** - Set up authentication methods * **[API Reference](/api-reference)** - Complete API documentation # AI Coding Skills Source: https://docs.riseworks.io/sdk/skills/ai-coding-skills Optional AI assistant skills that help generate correct Rise SDK code. Choose your agent, then add skills with one command. ## What are AI coding skills? **Skills** are instructions that AI coding assistants (Claude Code, Cursor, Windsurf, Codex, etc.) use to generate better, more accurate code. They describe when to use which APIs, how to structure requests, and what to avoid—so the assistant behaves like someone who already knows the Rise SDK. Rise ships **optional** skills for its SDK. If you add them to your project, your assistant gets Rise-specific guidance (payments, teams, webhooks, auth, and more). You don’t have to use them; if you do, you don’t need to read or edit any skill files—your tool loads them automatically. For the general format and how skills work across tools, see the [Agent Skills specification](https://agentskills.io/specification) and [Agent Skills documentation](https://agentskills.io). ## Choose your agent Skills work with these agents. When you add skills, you’ll pass the one you use: | Agent ID | Tool | | ---------- | ----------- | | `claude` | Claude Code | | `cursor` | Cursor | | `windsurf` | Windsurf | | `codex` | Codex | If you use a different AI coding tool, you can still add skills manually (see below). ## How to add skills From your project root (with `@riseworks/sdk` installed), run: ```bash theme={null} # Add all skills to your agent (replace with claude, cursor, windsurf, or codex) npx @riseworks/sdk add-skills --agent # Or add all skills to every supported agent npx @riseworks/sdk add-skills # Add only specific skills to one agent npx @riseworks/sdk add-skills --agent cursor rise-sdk-integration rise-webhooks # See available agents and skills npx @riseworks/sdk add-skills --list ``` Restart or refresh your AI tool after adding skills so it picks them up. **Using another agent?** Copy the contents of `node_modules/@riseworks/sdk/ai-skills/` into your tool’s skills (or rules) folder, then restart the tool. ## Available skills | Skill | When to use it | | ------------------------------- | ---------------------------------------------------------------------------------------------------- | | **rise-sdk-integration** | General Rise SDK usage—teams, payments, invites, balances, webhooks. Add this to every Rise project. | | **rise-payments-workflows** | Payments, bill pay, balances, withdrawals, payroll lookups. | | **rise-webhooks** | Webhook validation, event handling, and receiving endpoints. | | **rise-teams-and-invites** | Teams, members, invites, organizations. | | **rise-auth-and-setup** | Authentication, JWT vs Rise ID, environment setup. | | **rise-v1-migration** | Migrating from the legacy V1 API to B2B. | | **rise-debugging-and-errors** | Troubleshooting auth, payloads, and webhook issues. | | **rise-security-and-approvals** | Server-side secrets, approval flows, and safe patterns. | **Suggested approach:** Add `rise-sdk-integration` first. Add the others only if you’re actively working in those areas (e.g. `rise-v1-migration` when migrating from V1). ## Documentation For full API and SDK details, see the rest of this documentation and the [Rise B2B API docs](https://v2-docs.riseworks.io/). # Security Best Practices Source: https://docs.riseworks.io/security/best-practices Comprehensive security best practices for Rise B2B API integration Following these security best practices is essential for protecting your application and user data when integrating with Rise B2B API. ## Security Overview Implementing robust security measures is crucial for any API integration. This guide covers comprehensive security best practices for Rise B2B API integration. * Secure credential management * Multi-factor authentication * Token rotation * Access control * Encryption in transit and at rest * Secure data handling * Privacy compliance * Audit logging ## Authentication Best Practices ### Credential Management * Store all secrets in environment variables * Never hardcode credentials * Use secure secret management * Rotate credentials regularly * Implement principle of least privilege * Use dedicated API wallets * Monitor access patterns * Regular access reviews ### JWT Token Security ```typescript theme={null} // Good: Use environment variables const client = new RiseApiClient({ environment: 'prod', jwtToken: process.env.JWT_TOKEN }); // ❌ Bad: Hardcoded tokens const client = new RiseApiClient({ environment: 'prod', jwtToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' }); ``` ### Private Key Security ```typescript theme={null} // Good: Use dedicated API wallet const client = new RiseApiClient({ environment: 'prod', riseIdAuth: { riseId: process.env.API_WALLET_ADDRESS, privateKey: process.env.API_WALLET_PRIVATE_KEY } }); // ❌ Bad: Using personal wallet const client = new RiseApiClient({ environment: 'prod', riseIdAuth: { riseId: process.env.PERSONAL_WALLET_ADDRESS, privateKey: process.env.PERSONAL_WALLET_PRIVATE_KEY } }); ``` ## Data Protection ### Sensitive Data Handling * Encrypt data in transit (TLS 1.3) * Encrypt data at rest * Use strong encryption algorithms * Secure key management * Collect only necessary data * Anonymize when possible * Implement data retention policies * Regular data audits ### Secure Data Storage ```typescript theme={null} // Good: Secure data handling const handleUserData = (userData) => { // Log only non-sensitive information console.log('User ID:', userData.id); console.log('User role:', userData.role); // Never log sensitive data // console.log('Email:', userData.email); // ❌ Bad // console.log('Address:', userData.address); // ❌ Bad }; // ✅ Good: Mask sensitive data in logs const maskEmail = (email) => { const [local, domain] = email.split('@'); return `${local.charAt(0)}***@${domain}`; }; ``` ## Webhook Security ### Webhook Validation ```typescript theme={null} // ✅ Good: Always validate webhooks app.post('/webhooks/rise', (req, res) => { try { const isValid = validator.validateEvent( req.body, req.headers['rise-signature'] ); if (!isValid) { return res.status(400).json({ error: 'Invalid signature' }); } // Process webhook processWebhook(req.body); res.status(200).json({ received: true }); } catch (error) { console.error('Webhook validation error:', error); res.status(400).json({ error: error.message }); } }); // ❌ Bad: No validation app.post('/webhooks/rise', (req, res) => { // Process webhook without validation processWebhook(req.body); res.status(200).json({ received: true }); }); ``` ### Webhook Secret Management ```bash theme={null} # ✅ Good: Use environment variables WEBHOOK_SECRET=your_secure_webhook_secret_here # ❌ Bad: Hardcoded secret WEBHOOK_SECRET=abc123def456 ``` ## Network Security ### HTTPS Enforcement ```typescript theme={null} // ✅ Good: Enforce HTTPS in production if (process.env.NODE_ENV === 'production') { app.use((req, res, next) => { if (!req.secure && req.get('x-forwarded-proto') !== 'https') { return res.redirect(`https://${req.get('host')}${req.url}`); } next(); }); } ``` ### Rate Limiting ```typescript theme={null} import rateLimit from 'express-rate-limit'; // ✅ Good: Implement rate limiting const apiLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // limit each IP to 100 requests per windowMs message: 'Too many requests from this IP' }); app.use('/api/', apiLimiter); ``` ## Error Handling ### Secure Error Responses ```typescript theme={null} // ✅ Good: Don't expose sensitive information app.use((error, req, res, next) => { console.error('Error:', error); // Don't expose internal details res.status(500).json({ error: 'Internal server error', requestId: req.id // For tracking }); }); // ❌ Bad: Exposing sensitive information app.use((error, req, res, next) => { res.status(500).json({ error: error.message, stack: error.stack, // ❌ Exposes internal details sql: error.sql // ❌ Exposes database queries }); }); ``` ### Input Validation ```typescript theme={null} // ✅ Good: Validate all inputs const validatePaymentRequest = (data) => { const errors = []; if (!data.amount || isNaN(data.amount) || data.amount <= 0) { errors.push('Invalid amount'); } if (!data.recipients || !Array.isArray(data.recipients)) { errors.push('Invalid recipients'); } if (data.recipients && data.recipients.length > 10) { errors.push('Too many recipients'); } return errors; }; app.post('/api/payments', (req, res) => { const errors = validatePaymentRequest(req.body); if (errors.length > 0) { return res.status(400).json({ errors }); } // Process payment }); ``` ## Monitoring and Logging ### Security Monitoring ```typescript theme={null} // ✅ Good: Monitor security events const securityEvents = { failedLogins: 0, invalidTokens: 0, webhookFailures: 0, suspiciousActivity: [] }; // Monitor authentication failures app.use('/api/', (req, res, next) => { const originalSend = res.send; res.send = function(data) { if (res.statusCode === 401) { securityEvents.failedLogins++; if (securityEvents.failedLogins > 10) { // Alert security team alertSecurityTeam('Multiple failed login attempts'); } } originalSend.call(this, data); }; next(); }); ``` ### Audit Logging ```typescript theme={null} // ✅ Good: Comprehensive audit logging const auditLog = (action, userId, details) => { const logEntry = { timestamp: new Date().toISOString(), action, userId, ip: req.ip, userAgent: req.get('User-Agent'), details }; // Log to secure audit system auditLogger.log(logEntry); }; // Log sensitive operations app.post('/api/payments', (req, res) => { auditLog('payment_created', req.user.id, { amount: req.body.amount, recipients: req.body.recipients.length }); }); ``` ## Compliance and Standards ### Data Privacy * Data minimization * Right to be forgotten * Consent management * Data portability * Secure payment processing * Data encryption * Access controls * Regular audits ### Security Standards ```typescript theme={null} // ✅ Good: Implement security headers app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", "data:", "https:"], }, }, hsts: { maxAge: 31536000, includeSubDomains: true, preload: true } })); ``` ## Security Checklist ### Development Environment ✅ Use environment variables for all secrets ✅ Implement secure development practices ✅ Use HTTPS in development ✅ Regular dependency updates ✅ Input validation on all endpoints ✅ Secure error handling ✅ No hardcoded credentials ✅ Regular security code reviews ✅ Security testing in CI/CD ✅ Penetration testing ✅ Vulnerability scanning ✅ Regular security audits ### Production Environment ✅ Secure deployment pipeline ✅ Environment-specific configurations ✅ Secrets management ✅ Infrastructure security ✅ Security event monitoring ✅ Real-time alerting ✅ Log aggregation ✅ Performance monitoring ✅ Regular security updates ✅ Patch management ✅ Backup and recovery ✅ Incident response plan ## Incident Response ### Security Incident Plan ```typescript theme={null} // ✅ Good: Incident response framework const handleSecurityIncident = async (incident) => { // 1. Immediate containment await containIncident(incident); // 2. Assess impact const impact = await assessImpact(incident); // 3. Notify stakeholders await notifyStakeholders(incident, impact); // 4. Investigate root cause const rootCause = await investigateRootCause(incident); // 5. Implement fixes await implementFixes(rootCause); // 6. Document lessons learned await documentLessonsLearned(incident); }; ``` ### Recovery Procedures Stop affected services Isolate compromised systems Preserve evidence Identify scope of compromise Assess potential impact Document incident details Restore from secure backups Update compromised credentials Implement additional security Conduct post-incident review Update security procedures Implement lessons learned ## Security Resources ### Tools and Services * **Security Scanning**: OWASP ZAP, Snyk, SonarQube * **Monitoring**: Security Information and Event Management (SIEM) * **Testing**: Burp Suite, OWASP Testing Guide * **Compliance**: SOC 2, ISO 27001, GDPR tools ### Documentation * **[Security Overview](./overview)** - Complete security architecture * **[Private Keys](./private-keys)** - Private key security * **[Secondary Wallets](./secondary-wallets)** - Dedicated wallet usage * **[Webhook Validation](./webhook-validation)** - Webhook security ## Next Steps 1. **[Security Overview](./overview)** - Complete security architecture 2. **[Private Keys](./private-keys)** - Understanding private key security 3. **[Secondary Wallets](./secondary-wallets)** - Using dedicated wallets 4. **[Webhook Validation](./webhook-validation)** - Securing webhooks # Security Overview Source: https://docs.riseworks.io/security/overview Comprehensive security guide for Rise B2B API integration Security is paramount when integrating with financial APIs. This guide covers all security aspects of Rise B2B API integration. ## Security Architecture Rise B2B API implements a multi-layered security approach to protect your data and transactions: * JWT token-based authentication * SIWE blockchain signatures * Multi-factor authentication * Session management * TLS 1.3 encryption * End-to-end encryption * Data at rest encryption * Secure key management * Role-based access control * Permission-based operations * API key management * Audit logging * Real-time threat detection * Anomaly monitoring * Security event logging * Incident response ## Security Best Practices ### Authentication Security * Use environment variables * Implement token rotation * Monitor token expiration * Secure token storage * Use dedicated API wallets * Implement secure storage * Regular key rotation * Access control ### Data Protection * Never log sensitive data * Use secure transmission * Implement data masking * Regular data audits * Principle of least privilege * Regular access reviews * Multi-factor authentication * Session management ## Security Checklist ✅ Use environment variables for secrets ✅ Implement secure key storage ✅ Set up monitoring and alerts ✅ Configure access controls ✅ Use dedicated API wallets ✅ Implement token rotation ✅ Set up multi-factor auth ✅ Monitor authentication events ✅ Encrypt sensitive data ✅ Implement secure transmission ✅ Set up audit logging ✅ Regular security audits ✅ Set up security alerts ✅ Monitor API usage ✅ Track authentication events ✅ Implement incident response ## Security Features ### API Security * **Rate Limiting**: Prevents abuse and DDoS attacks * **Request Validation**: Validates all incoming requests * **CORS Protection**: Controls cross-origin access * **Input Sanitization**: Prevents injection attacks ### Webhook Security * **Signature Verification**: HMAC-SHA256 signatures * **Timestamp Validation**: Prevents replay attacks * **Secure Delivery**: TLS-encrypted webhook delivery * **Retry Logic**: Reliable webhook delivery ### Blockchain Security * **Cryptographic Signatures**: EIP-712 typed data signing * **Nonce Management**: Prevents replay attacks * **Chain Validation**: Ensures correct blockchain * **Gas Optimization**: Efficient transaction handling ## Compliance and Standards ### Security Standards * **SOC 2 Type II**: Service organization controls * **PCI DSS**: Payment card industry standards * **GDPR**: Data protection regulations * **ISO 27001**: Information security management ### Audit and Compliance * **Regular Audits**: Third-party security audits * **Penetration Testing**: Regular security assessments * **Vulnerability Management**: Continuous security monitoring * **Incident Response**: 24/7 security monitoring ## Incident Response ### Security Incidents If you suspect a security incident: Stop affected operations Isolate compromised systems Preserve evidence Identify scope of compromise Assess potential impact Document incident details Implement containment measures Update security controls Monitor for further activity Restore from secure backups Update compromised credentials Implement additional security Conduct post-incident review Update security procedures Implement lessons learned ### Contact Information * **Security Team**: [security@riseworks.io](mailto:security@riseworks.io) * **Emergency**: +1-XXX-XXX-XXXX * **Support**: [support@riseworks.io](mailto:support@riseworks.io) ## Security Resources ### Documentation * **[Private Keys](./private-keys)** - Understanding and securing private keys * **[Secondary Wallets](./secondary-wallets)** - Using dedicated wallets for API operations * **[Webhook Validation](./webhook-validation)** - Securing webhook communications * **[Best Practices](./best-practices)** - Comprehensive security guidelines ### Tools and Utilities * **SDK Security Features**: Built-in security validation * **Webhook Validator**: Secure webhook signature verification * **Security Monitoring**: Real-time security alerts * **Audit Logs**: Comprehensive activity logging ## Next Steps 1. **[Private Keys](./private-keys)** - Learn about private key security 2. **[Secondary Wallets](./secondary-wallets)** - Set up dedicated API wallets 3. **[Webhook Validation](./webhook-validation)** - Secure webhook integration 4. **[Best Practices](./best-practices)** - Implement security best practices # Private Key Security Source: https://docs.riseworks.io/security/private-keys Understanding and securing private keys for Rise B2B API Understanding private keys is crucial for secure authentication and transaction signing in blockchain-based applications. **A private key used with the Rise API can move money.** It's not only a login. Whoever holds the key of an authorized wallet can sign in as you and approve payments and withdrawals for every team that wallet's role reaches, and the Owner role can do everything the account can, including authorizing more wallets. Treat it like a production secret with access to funds: a dedicated wallet, stored in a secret manager or HSM, never in code, a repo, a log, or anything client-side. If it leaks, remove the wallet from **Security → Authorized Wallets** to revoke its on-chain role, then rotate to a new one. A leaked JWT expires in 24 hours; a leaked private key keeps working until you revoke the wallet. ## What is a Private Key? A private key is a cryptographic secret that allows you to: * **Sign messages** to prove your identity * **Authorize transactions** on the blockchain * **Control digital assets** associated with your wallet * **Authenticate** with blockchain-based services Think of it as a digital signature that only you can create, proving you are who you claim to be. ## How to Get Your Private Key ### From MetaMask Click on the MetaMask extension in your browser Click on the three dots menu → Account details Click "Export Private Key" and enter your password Copy the private key (starts with 0x) ### From Hardware Wallet Connect your hardware wallet to your computer Use your wallet's management software Follow your wallet's export process Ensure you're using a secure connection ### From Other Wallets Most wallets provide an export function: * **Trust Wallet**: Settings → Security → Export Private Key * **Coinbase Wallet**: Settings → Advanced → Export Private Key * **Rainbow**: Settings → Security → Export Private Key ### Generate New Key ```typescript theme={null} import { ethers } from 'ethers'; // Generate a new wallet const wallet = ethers.Wallet.createRandom(); // Get the private key const privateKey = wallet.privateKey; const address = wallet.address; console.log('Private Key:', privateKey); console.log('Address:', address); ``` ## Private Key Security Best Practices * Use password managers * Hardware security modules (HSM) * Encrypted storage * Never store in plain text * Limit access to authorized personnel * Use role-based access * Implement audit logging * Regular access reviews * Create secure backups * Use multiple locations * Test recovery process * Update backups regularly * Monitor wallet activity * Set up alerts * Regular security audits * Track usage patterns ## Private Key Format Private keys in Ethereum are: * **64 characters** long (32 bytes) * **Hexadecimal** format * **Start with 0x** * **Case sensitive** Example: `0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef` ## Why Private Keys Matter Private keys are the foundation of blockchain-based security and provide several critical benefits: * Mathematically unbreakable * Quantum-resistant algorithms * Zero-knowledge proofs * Tamper-evident signatures * You control your own identity * No reliance on third-party authentication * Direct ownership of digital assets * Self-sovereign identity * Blockchain-verifiable signatures * Immutable transaction history * Transparent audit logs * Compliance-ready records * Self-sovereign identity * Regulatory compliance * Audit trail requirements * Data privacy standards ## Private Key vs Traditional Authentication | Feature | Private Key (Blockchain) | Traditional Authentication | | --------------- | ------------------------ | -------------------------- | | **Security** | Cryptographic signatures | Password-based | | **Control** | User owns the key | Provider controls access | | **Recovery** | Self-managed backup | Provider-dependent | | **Portability** | Works across platforms | Platform-specific | | **Audit Trail** | Blockchain-verifiable | Provider logs | | **Compliance** | Self-sovereign | Provider compliance | ## Getting Started with Private Keys If you're new to blockchain-based authentication, follow these steps: Select a wallet that supports private key export Follow your wallet's export process Store the private key securely using environment variables Test with staging environment first Set up monitoring and alerts **Security Recommendation**: Use a dedicated secondary wallet for API operations. This wallet should contain minimal funds and be used exclusively for signing API transactions, keeping your primary wallet secure. ## Environment Variables ```bash theme={null} # .env file PRIVATE_KEY=0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef RISE_ID=0x1234567890123456789012345678901234567890 ``` ```typescript theme={null} // TypeScript configuration const client = new RiseApiClient({ environment: 'prod', riseIdAuth: { riseId: process.env.RISE_ID, privateKey: process.env.PRIVATE_KEY } }); ``` # Secondary Wallet Security Source: https://docs.riseworks.io/security/secondary-wallets Security principles and best practices for using secondary wallets with Rise B2B API Secondary wallets are a critical security practice for API operations. This guide covers the security principles and best practices. ## Security Principles ### Principle of Least Privilege Secondary wallets implement the principle of least privilege by: * **Limited scope**: Only used for specific API operations * **Minimal permissions**: No access to personal or business funds * **Controlled access**: Restricted to authorized API endpoints * **Audit trail**: All activities are logged and monitored ### Defense in Depth Using secondary wallets creates multiple security layers: * Remains secure and isolated * No API access or exposure * Protected from compromise * Maintains full control * Limited scope and access * Dedicated for API operations * Easy to replace if compromised * Clear separation of concerns ## Security Benefits ### Risk Mitigation * **Isolation**: API operations isolated from main funds * **Exposure reduction**: Limited attack surface * **Quick recovery**: Easy to replace compromised wallet * **Damage control**: Minimal impact from security incidents ### Compliance and Audit * **Clear separation**: Business vs. personal transactions * **Audit trails**: All API activities are traceable * **Regulatory compliance**: Meets separation requirements * **Documentation**: Clear purpose and usage records ## Security Best Practices ### Wallet Management * Limit access to authorized personnel * Implement role-based permissions * Regular access reviews * Secure key storage * Real-time activity monitoring * Automated alerts for unusual activity * Regular security audits * Transaction pattern analysis * Secure backup procedures * Multiple secure locations * Regular backup testing * Recovery documentation * Immediate compromise response * Wallet replacement procedures * Communication protocols * Post-incident analysis ### Security Checklist Generate new wallet with secure entropy Store private key in secure location Document wallet purpose and scope Implement strict access controls Use secure key management systems Regular access reviews and audits Configure real-time monitoring Set up automated alerts Establish incident response procedures Document security procedures Maintain incident response plans Regular security training ## Security Considerations ### Threat Models **Compromised API Credentials:** * Secondary wallet limits exposure * Easy to replace without affecting main funds * Clear audit trail for investigation **Malicious API Usage:** * Limited scope prevents widespread damage * Monitoring detects unusual patterns * Quick response and recovery procedures **Internal Threats:** * Access controls limit unauthorized use * Audit trails provide accountability * Separation reduces insider threat impact ### Compliance Requirements * **Regulatory separation**: Meets financial regulation requirements * **Audit trails**: Provides clear transaction history * **Risk management**: Demonstrates security best practices * **Documentation**: Supports compliance reporting ## Incident Response ### Compromise Detection * **Unusual activity**: Monitor for unexpected transactions * **Failed authentication**: Track authentication failures * **Pattern changes**: Detect deviations from normal usage * **Security alerts**: Automated monitoring and notifications ### Response Procedures Disable compromised wallet access Notify security team immediately Document incident details Analyze compromise scope and impact Review audit logs and monitoring data Identify root cause and vulnerabilities Generate new secondary wallet Update API configurations Restore secure operations Update security procedures Conduct team training Implement additional safeguards * **Document purpose**: Clearly label this wallet for API operations only ### Security Measures * **Separate storage**: Store private key separately from primary wallet * **Backup securely**: Create secure backups of the private key * **Access control**: Limit access to authorized personnel only * **Regular audits**: Review wallet activity and access logs ### Operational Guidelines * **Dedicated use**: Use exclusively for API operations * **No personal transactions**: Keep separate from personal crypto activities * **Clear labeling**: Mark all transactions as API-related * **Documentation**: Maintain clear records of wallet purpose and usage ## Recovery Procedures ### If Wallet is Compromised Stop all API operations immediately Review recent transactions Generate replacement wallet Update environment variables Test with new wallet # Webhook Validation Source: https://docs.riseworks.io/security/webhook-validation Secure webhook signature validation for Rise B2B API Webhook validation is crucial for ensuring the security and authenticity of incoming webhook events from Rise B2B API. ## Webhook Security Overview Webhooks provide real-time notifications but must be validated to ensure they come from Rise and haven't been tampered with. Our webhook validation uses HMAC-SHA256 signatures for security. * HMAC-SHA256 signatures * Timestamp validation * Replay attack prevention * Tamper detection * Authentic source verification * Data integrity assurance * Attack prevention * Compliance requirements ## Webhook Signature Format Rise sends webhooks with a signature header in this format: ``` Rise-Signature: t=1705312200,v1=abc123def456... ``` Where: * `t` = Unix timestamp * `v1` = HMAC-SHA256 signature ## Using the Webhook Validator ### Basic Validation ```typescript theme={null} import { WebhookValidator } from '@riseworks/sdk'; // Initialize validator with your webhook secret const validator = new WebhookValidator({ secret: process.env.WEBHOOK_SECRET }); // Express.js webhook endpoint app.post('/webhooks/rise', (req, res) => { try { // Validate the webhook signature const isValid = validator.validateEvent( req.body, req.headers['rise-signature'] ); if (isValid) { // Process the webhook console.log('Webhook validated:', req.body); res.status(200).json({ received: true }); } else { res.status(400).json({ error: 'Invalid signature' }); } } catch (error) { console.error('Webhook validation error:', error); res.status(400).json({ error: error.message }); } }); ``` ### Safe Validation (Returns Result) ```typescript theme={null} import { WebhookValidator } from '@riseworks/sdk'; const validator = new WebhookValidator({ secret: process.env.WEBHOOK_SECRET }); app.post('/webhooks/rise', (req, res) => { // Use safe validation that returns a result object const result = validator.validateEventSafe( req.body, req.headers['rise-signature'] ); if (result.valid) { // Process webhook console.log('Webhook processed:', req.body); res.status(200).json({ received: true }); } else { console.error('Webhook validation failed:', result.error); res.status(400).json({ error: result.error }); } }); ``` ## Manual Validation ### Parse Signature Header ```typescript theme={null} import { WebhookValidator } from '@riseworks/sdk'; const validator = new WebhookValidator({ secret: process.env.WEBHOOK_SECRET }); // Parse signature header manually const signatureHeader = req.headers['rise-signature']; const { timestamp, signature } = validator.parseSignatureHeader(signatureHeader); console.log('Timestamp:', timestamp); console.log('Signature:', signature); ``` ### Validate Timestamp ```typescript theme={null} // Check if webhook is within acceptable time range const tolerance = 300; // 5 minutes const now = Math.floor(Date.now() / 1000); if (Math.abs(now - timestamp) > tolerance) { throw new Error('Webhook timestamp too old'); } ``` ### Compare Signatures ```typescript theme={null} // Generate expected signature const expectedSignature = validator.generateSignature(req.body, timestamp); // Compare signatures securely const isValid = validator.compareSignatures(signature, expectedSignature); if (isValid) { console.log('Signature verified'); } else { console.log('Signature verification failed'); } ``` ## Complete Validation Example ```typescript theme={null} import { WebhookValidator } from '@riseworks/sdk'; import express from 'express'; const app = express(); app.use(express.json()); const validator = new WebhookValidator({ secret: process.env.WEBHOOK_SECRET, tolerance: 300 // 5 minutes }); app.post('/webhooks/rise', (req, res) => { try { // Validate webhook const isValid = validator.validateEvent( req.body, req.headers['rise-signature'] ); if (!isValid) { return res.status(400).json({ error: 'Invalid webhook signature' }); } // Process webhook based on event type const { event, data } = req.body; switch (event) { case 'payment.completed': handlePaymentCompleted(data); break; case 'payment.failed': handlePaymentFailed(data); break; case 'invite.accepted': handleInviteAccepted(data); break; case 'invite.rejected': handleInviteRejected(data); break; case 'invite.expired': handleInviteExpired(data); break; default: console.log('Unhandled event:', event); } res.status(200).json({ received: true }); } catch (error) { console.error('Webhook processing error:', error); res.status(500).json({ error: 'Internal server error' }); } }); function handlePaymentCompleted(data) { console.log('Payment completed:', data.payment_id); // Update database, send notifications, etc. } function handlePaymentFailed(data) { console.log('Payment failed:', data.payment_id); // Handle failed payment } function handleInviteAccepted(data) { console.log('Invite accepted:', data.invite_id); // Update team member status } app.listen(3000, () => { console.log('Webhook server running on port 3000'); }); ``` ## Error Handling ### Common Validation Errors ```typescript theme={null} try { const isValid = validator.validateEvent(req.body, req.headers['rise-signature']); } catch (error) { switch (error.message) { case 'Missing signature header': console.error('No Rise-Signature header found'); break; case 'Invalid signature format': console.error('Signature header format is invalid'); break; case 'Webhook timestamp too old': console.error('Webhook timestamp is outside tolerance window'); break; case 'Invalid signature': console.error('Signature verification failed'); break; default: console.error('Unknown validation error:', error.message); } } ``` ### Logging and Monitoring ```typescript theme={null} // Webhook validation metrics const webhookMetrics = { total: 0, valid: 0, invalid: 0, errors: [] }; app.post('/webhooks/rise', (req, res) => { webhookMetrics.total++; try { const isValid = validator.validateEvent( req.body, req.headers['rise-signature'] ); if (isValid) { webhookMetrics.valid++; // Process webhook } else { webhookMetrics.invalid++; res.status(400).json({ error: 'Invalid signature' }); return; } } catch (error) { webhookMetrics.errors.push({ timestamp: new Date().toISOString(), error: error.message }); res.status(400).json({ error: error.message }); return; } res.status(200).json({ received: true }); }); // Log metrics periodically setInterval(() => { console.log('Webhook metrics:', webhookMetrics); }, 60000); // Every minute ``` ## Security Best Practices ### Environment Configuration ```bash theme={null} # .env file WEBHOOK_SECRET=your_webhook_secret_here WEBHOOK_TOLERANCE=300 ``` ### Validation Configuration ```typescript theme={null} const validator = new WebhookValidator({ secret: process.env.WEBHOOK_SECRET, tolerance: parseInt(process.env.WEBHOOK_TOLERANCE || '300') }); ``` ### Security Checklist Store webhook secret securely Use environment variables Never commit secret to version control Rotate secrets regularly Validate all incoming webhooks Check timestamp tolerance Verify signature format Handle validation errors Log validation failures Monitor webhook activity Set up alerts for suspicious activity Track validation metrics Return appropriate HTTP status codes Log detailed error information Implement retry logic for failures Monitor error rates ## Testing Webhook Validation ### Test with Sample Data ```typescript theme={null} // Test webhook validation const testWebhook = { event: 'payment.completed', data: { payment_id: 'pay_123456789', amount: '1000.00', currency: 'USD' }, timestamp: Math.floor(Date.now() / 1000) }; // Generate test signature const testSignature = validator.generateSignature( testWebhook, testWebhook.timestamp ); // Test validation const isValid = validator.validateEvent(testWebhook, `t=${testWebhook.timestamp},v1=${testSignature}`); console.log('Test validation result:', isValid); ``` ### Unit Tests ```typescript theme={null} import { WebhookValidator } from '@riseworks/sdk'; describe('WebhookValidator', () => { let validator; beforeEach(() => { validator = new WebhookValidator({ secret: 'test-secret' }); }); test('should validate correct signature', () => { const payload = { event: 'test', data: {} }; const timestamp = Math.floor(Date.now() / 1000); const signature = validator.generateSignature(payload, timestamp); const signatureHeader = `t=${timestamp},v1=${signature}`; const isValid = validator.validateEvent(payload, signatureHeader); expect(isValid).toBe(true); }); test('should reject invalid signature', () => { const payload = { event: 'test', data: {} }; const signatureHeader = 't=1234567890,v1=invalid-signature'; const isValid = validator.validateEvent(payload, signatureHeader); expect(isValid).toBe(false); }); test('should reject old timestamp', () => { const payload = { event: 'test', data: {} }; const timestamp = Math.floor(Date.now() / 1000) - 600; // 10 minutes ago const signature = validator.generateSignature(payload, timestamp); const signatureHeader = `t=${timestamp},v1=${signature}`; const isValid = validator.validateEvent(payload, signatureHeader); expect(isValid).toBe(false); }); }); ``` ## Next Steps 1. **[Security Overview](./overview)** - Complete security architecture 2. **[Secondary Wallets](./secondary-wallets)** - Using dedicated wallets 3. **[Best Practices](./best-practices)** - Security best practices # Authentication Source: https://docs.riseworks.io/v1-legacy/authentication/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. `GET /v1/auth/api/siwe` returns a message for your wallet to sign. Sign the returned message using your wallet's private key. `POST /v1/auth/api/siwe` verifies the signature and returns a JWT valid for 24 hours. ## Which flow do I use? 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. 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. 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` Wallet address doing the signing. 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. 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`. ```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" ``` ```json theme={null} { "data": { "wallet": "0xYourWalletAddress", "message": "riseworks.io wants you to sign in with your Ethereum account:\n0xYourWalletAddress\n\n..." } } ``` 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. ## Verify SIWE signature **POST** `/v1/auth/api/siwe` Wallet address of the user. The original SIWE message returned by the GET step. The signature produced by signing `message` with the wallet's private key. ```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" }' ``` ```json theme={null} { "data": { "token": "eyJhbGciOiJIUzI1NiIs...", "expires_in": 86400, "wallet_address": "0xYourWalletAddress" } } ``` 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. 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. ## 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). If your account hasn't finished migrating yet, these calls are transparently forwarded to the original V1 backend — no behavior change on your end. # Invites Source: https://docs.riseworks.io/v1-legacy/invites/invites List and send contractor/client invites All invite endpoints here require `Authorization: Bearer ` — see [Authentication](/v1-legacy/authentication). ## List invites **GET** `/v1/invites` V1/V2 RiseID of the company/team. Legacy numeric id, V1/V2 RiseID, or nanoid of the company/team (alternative to `company_riseid`). At least one of `company_riseid` or `company_id` is required. Unlike other endpoints here, invites don't resolve a RiseAccount address — use the RiseID or nanoid instead (see [Identifier formats](/v1-legacy/overview#identifier-formats)). ```bash theme={null} curl -X GET "https://b2b-api.riseworks.io/v1/invites?company_riseid=te-abc123def456" \ -H "Authorization: Bearer " ``` ```json theme={null} { "data": [ { "invite_id": "in-abc123def456", "email": "newcontractor@example.com", "alias": null, "invite_alternate_value": "newcontractor@example.com", "converted": false, "role": "contractor", "anonymous": false, "uuid": "in-abc123def456", "avatar": null, "status": "pending" } ] } ``` `status` is one of `pending`, `awaiting_invite_acceptance`, `onboard_compliance_pending`, or `onboard_started`. `converted` is always `false` in the current implementation — it doesn't yet reflect actual invite-acceptance state, regardless of `status`. `uuid` is always identical to `invite_id` (the same value under two keys, kept for V1 response-shape parity). **SDK:** ```javascript theme={null} const invites = await client.v1Legacy.listInvites({ company_riseid: 'te-abc123def456' }); ``` ## Send invites **POST** `/v1/invites` Emails to invite — either bare strings or `{ email, external_reference_id? }` objects. Defaults to `false`. Not allowed for `role: "client"`. V1/V2 RiseID of the company/team. Legacy numeric id, V1/V2 RiseID, or nanoid of the company/team (alternative to `company_riseid`). One of `contractor`, `client`, `aor_contractor`, `team_employee`. ```bash theme={null} curl -X POST "https://b2b-api.riseworks.io/v1/invites" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "company_riseid": "te-abc123def456", "role": "contractor", "inviteList": [ "newcontractor@example.com", { "email": "another@example.com", "external_reference_id": "emp-123" } ] }' ``` ```json theme={null} { "data": { "failed": [], "countAdded": 2, "invited": ["newcontractor@example.com", "another@example.com"] } } ``` Already-pending invites are re-counted as `invited` (re-sent), not created again — matching V1's original behavior. Invalid or already-in-company emails show up in `failed` instead, as `{ "invite": "", "error": "" }` objects — not plain strings. `countAdded` equals `invited.length`, including re-sends, despite the name suggesting only newly-created invites. **SDK:** ```javascript theme={null} const result = await client.v1Legacy.sendInvites({ company_riseid: 'te-abc123def456', role: 'contractor', inviteList: ['newcontractor@example.com'], }); ``` ## Send warmed (pre-filled) invites **POST** `/v1/invites/warmed` Sends invites pre-filled with contractor details, skipping manual data entry during onboarding. Supports a `light` mode (name + email only) and a full mode (address, DOB, company data). Unlike [Send invites](#send-invites), there's no `anonymous` option here — warmed invites are always non-anonymous. Light mode only requires name and email. Array of users to invite — see shape below. V1/V2 RiseID of the company/team. Legacy numeric id, V1/V2 RiseID, or nanoid of the company/team (alternative to `company_riseid`). One of `contractor`, `team_employee`, `aor_contractor`. Can be overridden per-user. Each item in `users`: ```json theme={null} { "role": "contractor", "sumsub_sharetoken": null, "details": { "email": "newcontractor@example.com", "firstname": "Jane", "lastname": "Doe", "middlename": "A", "dob": "1990-01-01", "external_reference_id": "emp-123" }, "address": { "country": "US", "city": "Austin", "state": "TX", "address_line1": "123 Main St", "zip": "78701" } } ``` * `sumsub_sharetoken` (string, optional) — a Sumsub applicant share token. When present, `details.firstname`/`lastname`/`address` are no longer required (Sumsub already has them) — everything else in this section assumes it's omitted. * `details.firstname`/`lastname` are required unless `sumsub_sharetoken` is set. * `details.dob`, if present, must parse as a valid date. * `details.external_reference_id`, if present, must be 1–255 characters. * `address.country` must be a 2-letter uppercase code (e.g. `US`, `GB`) — anything else is rejected. * `address` is required in full mode (omitted when `light: true` or when `sumsub_sharetoken` is provided); `address_line1`/`address_line2`/`zip`/`timezone` are capped at 255 characters. * `role` here accepts `contractor`, `team_employee`, or `aor_contractor` only — unlike [Send invites](#send-invites), there's no `client` option. ```bash theme={null} curl -X POST "https://b2b-api.riseworks.io/v1/invites/warmed" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "light": true, "company_riseid": "te-abc123def456", "users": [ { "details": { "email": "newcontractor@example.com", "firstname": "Jane", "lastname": "Doe" } } ] }' ``` ```json theme={null} { "data": { "failed": [], "countAdded": 1, "invited": ["newcontractor@example.com"] } } ``` Same response shape as [Send invites](#send-invites) — `failed` items are `{ invite, error }` objects. **SDK:** ```javascript theme={null} const result = await client.v1Legacy.sendWarmedInvites({ light: true, company_riseid: 'te-abc123def456', users: [{ details: { email: 'newcontractor@example.com', firstname: 'Jane', lastname: 'Doe' } }], }); ``` ## Errors | Status | Cause | | ------ | ------------------------------------------------------------------------------------------------------ | | `400` | Missing/invalid parameters, self-invite attempt, or inviting into a V2-only company (no default team). | | `401` | Missing or expired JWT. | | `403` | Caller lacks invite permission on the company/team. | | `404` | Company or team not found. | Still stuck? See [Troubleshooting](/v1-legacy/troubleshooting). # Overview Source: https://docs.riseworks.io/v1-legacy/overview How the original Rise V1 API endpoints keep working after migration to the B2B platform If your integration was built against the original Rise V1 API, you do not need to rewrite it to keep working. Every V1 endpoint your code already calls is still live, at the exact same path, with the exact same request and response shape. Once your account finishes migrating to Rise's new infrastructure, these endpoints are served directly by the new platform instead of the old V1 backend — your integration doesn't need to notice the difference. A copy-paste-runnable script that authenticates, lists your teams, and sends a payment — start there if you just want working code. This section documents the V1 Legacy endpoints as a **reference for existing integrations**. If you're starting a new integration, use the [B2B API](/api-reference) instead — see the [Migration Guide](/guides/migrating-from-v1) for the V1 → B2B endpoint mapping. ## How it works Requests to `/v1/*` are transparently forwarded to the original V1 backend. Behavior is identical to calling V1 directly. The same `/v1/*` requests are served by Rise's new infrastructure. Paths, request fields, and response fields stay the same. No code changes required for either phase — the same request produces the same response shape throughout. ## Base URLs | Environment | Base URL | | ----------- | -------------------------------------- | | Production | `https://b2b-api.riseworks.io` | | Staging | `https://b2b-api.staging-riseworks.io` | ## Response format These endpoints keep V1's original response envelope — **not** the B2B API's `{ success, data }` wrapper: ```json theme={null} { "data": { } } ``` Errors from request validation use V1's original shape: ```json theme={null} { "error": "Bad Request", "message": { "type": "invalid_parameters", "errors": { "wallet": ["Missing parameter wallet"] } } } ``` Other errors (not found, forbidden, etc.) return: ```json theme={null} { "error": "Not Found", "message": "Team not found" } ``` One case breaks this pattern: when a company/team isn't enabled for B2B API access, `GET /v1/teams` and the impersonation flow on `POST /v1/auth/api/siwe` return `403` in the **B2B API's** envelope instead — `{ "success": false, "data": "Company is not enabled for B2B API access", "error_code": "B2B_ACCESS_DENIED" }`. If your error handling only checks for the `error`/`message` shape above, add a check for `error_code` too. ## Identifier formats Outside of authentication, every team/company/user identifier param here (`rise_id`, `payee`, `teamId`, `talentId`, `company_id`, `company_riseid`, `payee_riseid`, `payee_ids`, etc.) accepts **any** of the following interchangeably: | Format | Example | | -------------------- | -------------------------------------------------------------------------- | | V1 RiseID (address) | `0xabc...` | | V2 RiseID (address) | `0xdef...` | | Nanoid | `te-abc123def456`, `us-xyz789abc123`, `co-...` | | RiseAccount address | `0x123...` (the entity's on-chain smart account, distinct from its RiseID) | | Legacy numeric V1 id | `12345` (team/company endpoints only) | The server resolves whichever format you send to the same underlying entity — pick whatever your integration already has on hand, no conversion needed on your end. This is different from [Authentication](/v1-legacy/authentication)'s `rise_id`, which must specifically be a team/company RiseID (not a nanoid or RiseAccount address), since it's parsed out of a signed SIWE message rather than resolved server-side. ## Authentication All endpoints except the SIWE endpoints themselves require a bearer JWT, obtained via `POST /v1/auth/api/siwe` — see [Authentication](/v1-legacy/authentication). ```bash theme={null} Authorization: Bearer ``` ## Using the SDK The `@riseworks/sdk` client exposes every endpoint here under `client.v1Legacy`, alongside the same `client.teams`, `client.payments`, etc. namespaces used for the B2B API: ```bash theme={null} npm install @riseworks/sdk ``` ```javascript theme={null} const { RiseApiClient } = require('@riseworks/sdk'); const client = new RiseApiClient({ environment: 'prod', riseIdAuth: { riseId: process.env.RISE_ID, privateKey: process.env.PRIVATE_KEY, }, }); // Endpoints from this section const teams = await client.v1Legacy.listTeams(); // B2B endpoints — same client, same JWT const balance = await client.entityBalance.get({ nanoid: process.env.RISE_ID }); ``` See each endpoint page for the full `client.v1Legacy.*` method list — `getSiwe`/`verifySiwe`, `pay`/`preparePay`/`executePay`, `batchPay`/`prepareBatchPay`/`executeBatchPay`, `createBatchPaymentIntents`, `listPayments`, `listTeams`, `listTalent`/`getTalent`/`removeTalent`, `getBalance`, `listInvites`/`sendInvites`/`sendWarmedInvites`. The example above uses `riseIdAuth` with a **personal** RiseID. Most API integrations instead authenticate a delegate wallet directly against a **team or company** RiseID — see [Authentication](/v1-legacy/authentication) for that flow, since it requires calling `getSiwe`/`verifySiwe` manually. ## Endpoint groups SIWE message generation and signature verification Instant and batch payments, payment intents, and payment history List teams, list/get/remove contractors Query on-chain USD balance for a team, user, or company List and send contractor/client invites, including pre-filled (warmed) invites Common errors across every endpoint, and how to fix them These endpoints are supported for existing integrations only and will eventually be deprecated. Plan to move to the [B2B API](/api-reference) when convenient — there's no urgency, but new work should target B2B directly. # Payments Source: https://docs.riseworks.io/v1-legacy/payments/payments Instant payments, batch payments, payment intents, and payment history These payments use the same two-step **prepare → execute** flow as the original V1 API: a `PUT` request returns EIP-712 typed data to sign, then a `POST` request submits the signature to broadcast the transaction. All payment endpoints here require `Authorization: Bearer ` — see [Authentication](/v1-legacy/authentication). `rise_id`/`payee`/`recipient`/`payee_riseid` accept a V1 RiseID, V2 RiseID, RiseAccount address, or nanoid interchangeably — see [Identifier formats](/v1-legacy/overview#identifier-formats). Every `amount` field on this page (`amount`, `total_amount`, `pay_intents[].amount`) is an integer in **micro-USD**, V1's original fixed-point unit — `1,000,000` = `$1.00`. A $100.00 payment is `"amount": 100000000`, not `100.00`. Sending a decimal dollar value like `100.00` is parsed as `100` micro-USD (**$0.0001\*\*) — it won't error, it'll just silently pay the wrong amount. ## Prepare an instant payment **PUT** `/v1/payments/pay` Wallet address of the payer. V1/V2 RiseID, RiseAccount address, or nanoid of the paying team. V1/V2 RiseID, RiseAccount address, or nanoid of the payee. Payment amount in micro-USD (`1,000,000` = `$1.00`) — e.g. `100000000` for \$100.00. Unique salt for this payment. Combined with `rise_id`/`payee`/`amount` to derive the on-chain payment id — reusing a salt for the same payer+payee+amount collides with the existing payment and fails with `409`. Use a new, never-before-used salt (e.g. a counter or random value) per payment. ```bash theme={null} curl -X PUT "https://b2b-api.riseworks.io/v1/payments/pay" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "wallet": "0xYourWalletAddress", "rise_id": "te-abc123def456", "payee": "us-xyz789abc123", "amount": 100000000, "salt": 1 }' ``` ```json theme={null} { "data": { "domain": { "name": "RiseAccountForwarder", "version": "1.0.0", "chainId": 42161, "verifyingContract": "0x..." }, "types": { "RisePayNow": [ { "name": "from", "type": "address" }, { "name": "to", "type": "address" }, { "name": "amount", "type": "uint256" }, { "name": "salt", "type": "uint256" } ] }, "message": { "from": "0x...", "to": "0x...", "amount": "100000000", "salt": "1" }, "primaryType": "RisePayNow", "descriptor": {} } } ``` `domain`, `types`, and `message` are the three arguments EIP-712 signing needs — see [Creating the signature](#creating-the-signature) below. `descriptor` is always an empty object — a V1 response-shape placeholder with no V2 equivalent. If this payee already received a very similar payment recently (same team, similar amount, within a lookback window), the response also includes a `duplicates` field — this doesn't block the payment, it's an early warning so you can confirm before signing: ```json theme={null} { "duplicates": { "days_checked": 30, "payments": [ { "to": "us-xyz789abc123", "amount_cents": 10000, "currency_symbol": "USD", "invoice_description": "", "payment_details": "", "external_id": "", "created_at": "2024-12-15T00:00:00.000Z" } ] } } ``` `duplicates` is omitted entirely when none are found. ## Execute an instant payment **POST** `/v1/payments/pay` Same body as the prepare step, plus the signed request and signature: The typed-data request object returned by the prepare step. Signature over the typed data. Optional payment title (max 60 chars). Optional payment description (max 1000 chars). Optional caller-supplied external reference id. Not length-checked at request time — values over 500 chars are silently truncated, not rejected. (`/v1/payments/batch-pay/intents` is stricter: it hard-validates a 1000-char max instead.) ```json theme={null} { "data": { "to": "0x...", "from": "0x...", "contractAddress": null, "transactionIndex": 0, "gasUsed": { "type": "BigNumber", "hex": "0x..." }, "blockHash": "0x...", "transactionHash": "0x...", "logs": [ ], "blockNumber": 123456, "confirmations": 1, "status": 1, "type": 2, "byzantium": true } } ``` This is an ethers-v5-shaped transaction receipt, matching V1's original response exactly. ## Creating the signature `domain`, `types`, and `message` from the prepare response feed directly into EIP-712 signing. This same pattern signs both instant and batch payments — just swap the endpoint. ### Using the SDK (recommended) `client.v1Legacy.pay()` runs prepare → sign → execute in a single call, using the client's configured key (or a per-call `privateKey` override): ```javascript theme={null} const { RiseApiClient } = require('@riseworks/sdk'); const { ethers } = require('ethers'); const client = new RiseApiClient({ environment: 'prod', riseIdAuth: { riseId: process.env.RISE_ID, privateKey: process.env.PRIVATE_KEY, }, }); // `wallet` is the address that will sign the payment — the same wallet // backing riseIdAuth.privateKey — not a RiseID. const wallet = new ethers.Wallet(process.env.PRIVATE_KEY); const receipt = await client.v1Legacy.pay({ wallet: wallet.address, rise_id: 'te-abc123def456', payee: 'us-xyz789abc123', amount: 100000000, // $100.00 in micro-USD salt: 1, }); console.log('Transaction hash:', receipt.data.transactionHash); ``` For manual control over signing, use `preparePay`/`executePay` directly, mirroring the same three steps: ```javascript theme={null} const prepared = await client.v1Legacy.preparePay({ wallet: wallet.address, rise_id: 'te-abc123def456', payee: 'us-xyz789abc123', amount: 100000000, salt: 1, }); const { domain, types, message } = prepared.data; const { signature } = await client.signTypedData(domain, types, message); const receipt = await client.v1Legacy.executePay({ wallet: wallet.address, rise_id: 'te-abc123def456', payee: 'us-xyz789abc123', amount: 100000000, salt: 1, request: message, signature, }); ``` `client.v1Legacy.batchPay()` / `prepareBatchPay()` / `executeBatchPay()` follow the identical pattern for `/v1/payments/batch-pay`. ### 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); const headers = { Authorization: `Bearer ${token}`, // from the authentication flow 'Content-Type': 'application/json', }; async function payInstant(riseId, payee, amount, salt) { // Step 1: Prepare — get EIP-712 typed data to sign const prepareResponse = await fetch(`${baseUrl}/v1/payments/pay`, { method: 'PUT', headers, body: JSON.stringify({ wallet: wallet.address, rise_id: riseId, payee, amount, salt, }), }); const { data } = await prepareResponse.json(); const { domain, types, message } = data; // Step 2: Sign the typed data with your wallet const signature = await wallet.signTypedData(domain, types, message); // Step 3: Execute — submit the signature to broadcast the payment const executeResponse = await fetch(`${baseUrl}/v1/payments/pay`, { method: 'POST', headers, body: JSON.stringify({ wallet: wallet.address, rise_id: riseId, payee, amount, salt, request: message, signature, }), }); const { data: receipt } = await executeResponse.json(); return receipt; // ethers-v5-shaped transaction receipt } const receipt = await payInstant('te-abc123def456', 'us-xyz789abc123', 100000000, 1); console.log('Transaction hash:', receipt.transactionHash); ``` The same `domain`/`types`/`message` → `wallet.signTypedData()` → submit-as-`request` pattern applies to `/v1/payments/batch-pay` and `/v1/payments/batch-pay/intents` as well. ## Prepare a batch payment **PUT** `/v1/payments/batch-pay` Wallet address of the payer. V1/V2 RiseID, RiseAccount address, or nanoid of the paying team. Micro-USD. Must equal the sum of all `payments[].amount`. Array of `{ recipient, amount, salt }` — 1 to 1000 items. Each `salt` must be unique per `recipient` + `amount` (across this batch and any prior payment) — reused salts fail with `409`. ```bash theme={null} curl -X PUT "https://b2b-api.riseworks.io/v1/payments/batch-pay" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "wallet": "0xYourWalletAddress", "rise_id": "te-abc123def456", "total_amount": 200000000, "payments": [ { "recipient": "us-xyz789abc123", "amount": 100000000, "salt": 1 }, { "recipient": "us-def456ghi789", "amount": 100000000, "salt": 2 } ] }' ``` ```json theme={null} { "data": { "domain": { "name": "RiseAccountForwarder", "version": "1.0.0", "chainId": 42161, "verifyingContract": "0x..." }, "types": { "RisePayNowBatchRequest": [ { "name": "from", "type": "address" }, { "name": "payments", "type": "PaymentItem[]" } ] }, "message": { "from": "0x...", "payments": [ ] }, "primaryType": "RisePayNowBatchRequest" } } ``` Sign with `wallet.signTypedData(domain, types, message)` — see [Creating the signature](#creating-the-signature) above. Unlike instant-pay, there's no `descriptor` field here — but `duplicates` can still appear, in the same shape [documented above](#prepare-an-instant-payment). ## Execute a batch payment **POST** `/v1/payments/batch-pay` Same body as the prepare step, with each payment item optionally carrying `title` (≤60 chars), `description` (≤1000 chars), and `external_id` (silently truncated over 500 chars, not rejected), plus `request` and `signature`. Returns the same ethers-v5 receipt shape as the instant-payment execute step. ## Create batch payment intents **POST** `/v1/payments/batch-pay/intents` Creates draft (unscheduled) payments — useful for scheduling future payroll without signing immediately. Legacy numeric id, V1/V2 RiseID, RiseAccount address, or nanoid of the paying company/team. Array of intents, 1 to 1000 items, each: `{ payee_riseid, amount, salt, timestamp, title?, description?, external_id? }`. `amount` is micro-USD and must be **greater than 0** (the only payment endpoint that rejects `0`, where every other one only requires non-negative). `title` ≤60 chars, `description` ≤1000 chars, `external_id` ≤1000 chars. ```bash theme={null} curl -X POST "https://b2b-api.riseworks.io/v1/payments/batch-pay/intents" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "company_id": "te-abc123def456", "pay_intents": [ { "payee_riseid": "us-xyz789abc123", "amount": 100000000, "salt": 1, "timestamp": 1735689600 } ] }' ``` ```json theme={null} { "data": [ { "id": "pa-abc123def456", "payHash": "0x...", "amountUSD": "100.00", "paySchedule": "0x...", "approvedPayDate": "2025-01-01T00:00:00.000Z", "saltWeekOrSchedule": "1", "company": "te-abc123def456", "payee": "us-xyz789abc123", "status": "ready", "created_at": "2024-12-01T00:00:00.000Z", "updated_at": "2024-12-01T00:00:00.000Z", "uid": "us-xyz789abc123" } ] } ``` Returns `409 Conflict` if a payment group or payment with the same salt already exists — use a new salt to retry. **SDK:** ```javascript theme={null} const intents = await client.v1Legacy.createBatchPaymentIntents({ company_id: 'te-abc123def456', pay_intents: [{ payee_riseid: 'us-xyz789abc123', amount: 100000000, salt: 1, timestamp: 1735689600 }], }); ``` ## List payments **GET** `/v1/payments` Legacy numeric id, V1/V2 RiseID, RiseAccount address, or nanoid of the paying company/team. Filter by one or more caller-supplied external ids. Filter by one or more payees — V1/V2 RiseID, RiseAccount address, or nanoid. 1-indexed page number (default 1). Page size, 1–1000 (default 100). Values above 1000 are silently capped, not rejected. `all`, a V2 state name (`intent`/`ready`, `scheduled`, `complete`, `removed`), or a V1 status (`draft`, `pending`, `ready`, `scheduled`, `submitted`, `complete`, `failed`, `attempted_and_failed`, `removed`, `rejected`, `cancelled`). `draft`, `pending`, `submitted`, `failed`, `attempted_and_failed`, `rejected`, and `cancelled` pass validation (they're valid V1 statuses) but have no V2 equivalent — filtering by any of them returns `200` with `"data": []` rather than an error. If a filtered call unexpectedly returns nothing, check that `status` is one of the ones with a real V2 mapping. ```bash theme={null} curl -X GET "https://b2b-api.riseworks.io/v1/payments?company_id=te-abc123def456&status=all&page=1&count=100" \ -H "Authorization: Bearer " ``` ```json theme={null} { "data": [ { "amount_usd": "100.00", "approved_pay_date": "2025-01-01T00:00:00.000Z", "pay_hash": "0x...", "salt": "1", "status": "complete", "external_id": null, "transaction_hash": "0x...", "payee": { "id": "us-xyz789abc123", "avatar": null, "email": "payee@example.com", "name": "Jane Payee", "address": "0x..." } } ] } ``` **SDK:** ```javascript theme={null} const payments = await client.v1Legacy.listPayments({ company_id: 'te-abc123def456', status: 'all' }); ``` ## Errors | Status | Cause | | ------ | --------------------------------------------------------------------------------------------------------------------------------- | | `400` | Missing/invalid parameters (see the `errors` object in the response). | | `401` | Missing or expired JWT. | | `403` | Caller lacks `WRITE_TEAM_PAYROLL` permission on the paying team, or the company isn't enabled for B2B API access. | | `404` | Payer account, payee, or team not found. | | `409` | A payment group or payment with the same `salt` (for this payer + payee + amount) already exists — retry with a new, unused salt. | | `504` | (execute endpoints only) Transaction wasn't confirmed in time — check status via `GET /v1/payments` before retrying. | Still stuck? See [Troubleshooting](/v1-legacy/troubleshooting). # Quickstart Source: https://docs.riseworks.io/v1-legacy/quickstart Authenticate and send your first payment in under 10 minutes This walks through the most common integration pattern end-to-end: a dedicated wallet authenticates *as* a manager on your team/company, then lists teams and sends a payment. Every step is copy-paste runnable. New here? [Overview](/v1-legacy/overview) explains what this section is and why it exists. This page assumes you just want working code. ## Prerequisites * **An existing Rise team or company** (from before your V1 integration) and its **V1 RiseID** — an address like `0xabc...`, found in the Rise dashboard under company/team settings. * **A wallet registered as that team/company's Owner or a delegate** — see [Secondary Wallets](/authentication/secondary-wallets) if you need to set one up. This wallet's private key does the signing; it doesn't need any funds. * **The email of a manager on that team** — a user with the `team_admin`, `team_finance_admin`, `team_delegate_admin`, or `team_payment_initiator` role. Can be the same person who controls the wallet, or someone else. * **Node.js 16+** Don't have those on hand, or not sure what they mean? See [Which flow do I use?](/v1-legacy/authentication#which-flow-do-i-use) — there's also a simpler "personal auth" flow for a wallet tied to your own individual Rise user account. ## Step 1: Install dependencies ```bash theme={null} mkdir v1-legacy-integration cd v1-legacy-integration npm init -y npm install @riseworks/sdk ethers dotenv ``` ## Step 2: Configure your environment Create a `.env` file: ```bash theme={null} RISE_ENVIRONMENT=prod # or 'stg' for staging WALLET_PRIVATE_KEY=0x... # the delegate/owner wallet's private key TEAM_RISE_ID=0x... # your team or company's V1 RiseID MANAGER_EMAIL=manager@example.com ``` Never commit `.env` to version control. Anyone with `WALLET_PRIVATE_KEY` can sign on your team/company's behalf. ## Step 3: Authenticate and list your teams Save this as `quickstart.js` and run it to confirm authentication works end-to-end: ```javascript theme={null} // quickstart.js require('dotenv').config(); const { RiseApiClient } = require('@riseworks/sdk'); const { ethers } = require('ethers'); async function main() { const wallet = new ethers.Wallet(process.env.WALLET_PRIVATE_KEY); // jwtToken is required to construct a client, but any placeholder works — // updateToken() below replaces it with the real one. const client = new RiseApiClient({ environment: process.env.RISE_ENVIRONMENT || 'prod', jwtToken: 'pending', }); console.log('Requesting SIWE message...'); const { data } = await client.v1Legacy.getSiwe({ wallet: wallet.address, rise_id: process.env.TEAM_RISE_ID, impersonate: process.env.MANAGER_EMAIL, }); console.log('Signing message with wallet', wallet.address); const signature = await wallet.signMessage(data.message); console.log('Verifying signature and exchanging for a JWT...'); const { data: tokenData } = await client.v1Legacy.verifySiwe({ wallet: wallet.address, message: data.message, signature, }); client.updateToken(tokenData.token); console.log('Authenticated. Token expires in', tokenData.expires_in, 'seconds.\n'); console.log('Fetching teams...'); const teams = await client.v1Legacy.listTeams(); console.log(`Found ${teams.data.length} team(s):`, teams.data.map((t) => t.riseId)); } main().catch((err) => { console.error('Quickstart failed:', err.message); process.exit(1); }); ``` ```bash theme={null} node quickstart.js ``` If this prints at least one team, authentication is working correctly — move on to Step 4. If it fails, jump to [Troubleshooting](#troubleshooting) below. ## Step 4: Send your first payment Add a payee nanoid or RiseID to your `.env` (any contractor already on the team you listed above), then extend the script: ```bash theme={null} # add to .env PAYEE_ID=us-xyz789abc123 ``` ```javascript theme={null} // append to quickstart.js, inside main(), after the listTeams() call console.log('\nSending a test payment...'); const receipt = await client.v1Legacy.pay( { wallet: wallet.address, rise_id: process.env.TEAM_RISE_ID, payee: process.env.PAYEE_ID, amount: 1000000, // $1.00, in micro-USD (1,000,000 = $1.00) — change before running against prod salt: Date.now(), // must be unique per payer+payee+amount — see Step 4 note below }, process.env.WALLET_PRIVATE_KEY, // client has no configured signing key, so pass it explicitly ); console.log('Payment sent. Transaction hash:', receipt.data.transactionHash); ``` `salt` must be unique for every payment to the same payee with the same amount — reusing one fails with `409 Conflict`. `Date.now()` is fine for testing; use a counter or UUID-derived integer in production. See [Payments](/v1-legacy/payments#prepare-an-instant-payment). Test against **staging** first (`RISE_ENVIRONMENT=stg` and `https://b2b-api.staging-riseworks.io`) before pointing this at production — see [Base URLs](/v1-legacy/overview#base-urls). ## Troubleshooting Either `wallet` isn't registered as the RiseID's Owner/delegate, or `impersonate` isn't a user with a manager role on that RiseID. Both are checked independently — see [Which flow do I use?](/v1-legacy/authentication#which-flow-do-i-use). The JWT is missing or expired (tokens last 24 hours). Re-run the authentication step to get a fresh one, and make sure `client.updateToken(...)` ran before the failing call. You reused a `salt` for the same payer + payee + amount. Use a new, never-before-used salt and retry. Every non-auth identifier (`rise_id`, `payee`, `teamId`, etc.) accepts a V1 RiseID, V2 RiseID, nanoid, or RiseAccount address interchangeably — no conversion needed. See [Identifier formats](/v1-legacy/overview#identifier-formats). See [Troubleshooting](/v1-legacy/troubleshooting) for the full list of errors across every endpoint. ## Next steps Full reference for both auth flows, manual HTTP, and error cases Batch payments, payment intents, and payment history List, get, and remove contractors Common errors and how to fix them, across every endpoint # RiseID Balance Source: https://docs.riseworks.io/v1-legacy/riseid-balance/riseid-balance Query on-chain USD balance for a team, user, or company RiseID Requires `Authorization: Bearer ` — see [Authentication](/v1-legacy/authentication). ## Get balance **GET** `/v1/riseid/{rise_id}/balance` Legacy numeric id, V1/V2 RiseID, RiseAccount address, or team/user/company nanoid — see [Identifier formats](/v1-legacy/overview#identifier-formats). ```bash theme={null} curl -X GET "https://b2b-api.riseworks.io/v1/riseid/te-abc123def456/balance" \ -H "Authorization: Bearer " ``` ```json theme={null} { "data": "150000000" } ``` The balance is returned as a raw decimal string in the token's smallest on-chain unit — the same raw format V1 returned. Convert using the token's decimals before displaying to users. **SDK:** ```javascript theme={null} const balance = await client.v1Legacy.getBalance('te-abc123def456'); ``` ## Errors | Status | Cause | | ------ | -------------------------------------------------------------------------------------------------------------------------------------------- | | `400` | Invalid `rise_id` format, or no on-chain USD contract is configured for this environment (a server-side config issue, not a caller mistake). | | `401` | Missing or expired JWT. | | `404` | Entity or its RiseAccount not found. | Still stuck? See [Troubleshooting](/v1-legacy/troubleshooting). # Teams & Talent Source: https://docs.riseworks.io/v1-legacy/teams/teams List teams and manage contractors All team endpoints here require `Authorization: Bearer ` — see [Authentication](/v1-legacy/authentication). `teamId`/`talentId` accept a legacy numeric id, V1 RiseID, V2 RiseID, RiseAccount address, or nanoid interchangeably — see [Identifier formats](/v1-legacy/overview#identifier-formats). ## List teams **GET** `/v1/teams` Lists every team the authenticated user is associated with (as payer or admin). ```bash theme={null} curl -X GET "https://b2b-api.riseworks.io/v1/teams" \ -H "Authorization: Bearer " ``` ```json theme={null} { "data": [ { "id": "123", "riseId": "te-abc123def456", "name": "Engineering Team", "owner": "us-xyz789abc123", "address1": null, "address2": null, "city": null, "state": null, "zip": null, "country": "US", "phone": null, "website": null, "avatar": null, "colors": null, "tax_id": null, "usWork": 1, "hasAMLCheck": true, "incorporationState": null, "incorporationType": null, "created_at": "2024-01-01T00:00:00.000Z", "updated_at": "2024-01-01T00:00:00.000Z" } ] } ``` Returns `403 Forbidden` in the [B2B\_ACCESS\_DENIED envelope](/v1-legacy/overview#response-format) if the user has team memberships but every owning company is disabled for B2B API access — matching V1's original behavior. If the user simply has no team memberships at all, this returns `200` with `"data": []`, not a 403. Teams owned by a company with no B2B-access record at all (never explicitly enabled or disabled) are also silently left out of `data` rather than causing an error. **SDK:** ```javascript theme={null} const teams = await client.v1Legacy.listTeams(); ``` ## List team talent **GET** `/v1/teams/{teamId}/talent` Lists a team's contractors, optionally filtered by email and paginated. Only `contractor`/`aor_contractor` roles are included — `team_employee` members exist on the team but never appear here (they also can't be looked up via [Get a single talent](#get-a-single-talent) below, though they *can* be removed via [DELETE](#remove-a-talent-from-a-team)). Legacy numeric id, V1/V2 RiseID, RiseAccount address, or team nanoid. Filter talent by email. 1-indexed page number. Pagination only activates when `page` or `offset` is set. Row offset (alternative to `page`). Page size, 1–500 (default 100). ```bash theme={null} curl -X GET "https://b2b-api.riseworks.io/v1/teams/te-abc123def456/talent?email=contractor@example.com" \ -H "Authorization: Bearer " ``` ```json theme={null} { "data": [ { "id": "456", "nanoid": "us-xyz789abc123", "riseId": "us-xyz789abc123", "email": "contractor@example.com", "firstname": "Jane", "lastname": "Doe", "middlename": null, "alias": null, "role": "contractor", "phone": null, "avatar": null, "city": null, "state": null, "country": null, "usWork": 0, "hasKYC": 1, "hasPSASigned": true, "external_reference_id": null, "created_at": "2024-01-01T00:00:00.000Z", "updated_at": "2024-01-01T00:00:00.000Z" } ] } ``` When `page`/`offset` is supplied, a `pagination` object is also included: ```json theme={null} { "data": [ ], "pagination": { "page": 1, "count": 100, "offset": 0, "has_more": false, "next_page": null, "next_offset": null } } ``` If a talent owns their own company, their object also includes a `company` field with that company's data. The key is **omitted entirely** when there's no owned company — it's never present-but-`null`. **SDK:** ```javascript theme={null} const talent = await client.v1Legacy.listTalent('te-abc123def456', { email: 'contractor@example.com' }); ``` ## Get a single talent **GET** `/v1/teams/{teamId}/talent/{talentId}` Legacy numeric id, V1/V2 RiseID, RiseAccount address, or team nanoid. Legacy numeric id, V1/V2 RiseID, RiseAccount address, or user nanoid. ```bash theme={null} curl -X GET "https://b2b-api.riseworks.io/v1/teams/te-abc123def456/talent/us-xyz789abc123" \ -H "Authorization: Bearer " ``` ```json theme={null} { "data": { "id": "456", "nanoid": "us-xyz789abc123", "email": "contractor@example.com", "firstname": "Jane", "lastname": "Doe", "role": "contractor" } } ``` Same object shape as one item from [List team talent](#list-team-talent) — including the conditional `company` field. **SDK:** ```javascript theme={null} const talent = await client.v1Legacy.getTalent('te-abc123def456', 'us-xyz789abc123'); ``` ## Remove a talent from a team **DELETE** `/v1/teams/{teamId}/talent/{talentId}` Terminates the relationship. Valid for `contractor`, `aor_contractor`, and `team_employee` roles — a broader set than [List team talent](#list-team-talent)/[Get a single talent](#get-a-single-talent), which only ever surface `contractor`/`aor_contractor`. This means a `team_employee` can be removed via this endpoint even though they never appear in those other two. Legacy numeric id, V1/V2 RiseID, RiseAccount address, or team nanoid. Legacy numeric id, V1/V2 RiseID, RiseAccount address, or user nanoid. ```bash theme={null} curl -X DELETE "https://b2b-api.riseworks.io/v1/teams/te-abc123def456/talent/us-xyz789abc123" \ -H "Authorization: Bearer " ``` ```json theme={null} { "data": "ok" } ``` **SDK:** ```javascript theme={null} await client.v1Legacy.removeTalent('te-abc123def456', 'us-xyz789abc123'); ``` ## Errors | Status | Cause | | ------ | -------------------------------------------------------------------------------- | | `400` | Invalid `teamId`/`talentId` format, or the talent isn't a contractor (`DELETE`). | | `401` | Missing or expired JWT. | | `403` | Caller lacks `READ_TEAM_PAYROLL`/`WRITE_TEAM_PAYROLL` permission on the team. | | `404` | Team or talent not found. | Still stuck? See [Troubleshooting](/v1-legacy/troubleshooting). # Troubleshooting Source: https://docs.riseworks.io/v1-legacy/troubleshooting Diagnose and fix common issues across these endpoints Most issues here fall into a handful of categories: picking the wrong auth flow, using an identifier the server can't resolve, or reusing a payment salt. This page walks through the fixes. ## Quick diagnosis checklist If your wallet belongs to an individual person who already administers teams/companies in the Rise dashboard, use **personal auth** (`wallet` only). If your wallet is a dedicated integration wallet registered on a specific team/company (the common case for API integrations), use **company/team-scoped auth** (`wallet` + `rise_id` + `impersonate`). See [Which flow do I use?](/v1-legacy/authentication#which-flow-do-i-use) for the full breakdown, or just follow the [Quickstart](/v1-legacy/quickstart), which uses the company/team-scoped flow. 403 on the SIWE endpoints and 403 on every other endpoint mean different things: * **On `GET`/`POST /v1/auth/api/siwe`**: either `wallet` isn't registered as the target RiseID's Owner/delegate, or `impersonate` isn't a user with a manager role (`team_admin`, `team_finance_admin`, `team_delegate_admin`, `team_payment_initiator`) on that RiseID. * **On every other endpoint**: your JWT is valid, but the impersonated user lacks the specific permission that endpoint requires (e.g. `WRITE_TEAM_PAYROLL` for payments). Double-check which manager role you impersonated with. * **Either case**: the company might not be enabled for B2B API access at all — this returns `{ "error_code": "B2B_ACCESS_DENIED" }`. Contact Rise support if you believe it should be. You don't need to convert anything. `rise_id`, `payee`, `teamId`, `talentId`, `company_id`, and similar params all accept a V1 RiseID, V2 RiseID, nanoid, or RiseAccount address interchangeably (invites is the one exception — see [Identifier formats](/v1-legacy/overview#identifier-formats)). Send whatever format your system already stores. You reused a `salt` value for the same payer + payee + amount combination — the server treats this as a duplicate of an existing payment. Use a new, never-before-used salt (a counter or timestamp works) and retry. See [Payments](/v1-legacy/payments#prepare-an-instant-payment). The transaction was broadcast but wasn't confirmed on-chain before the request timed out — it may still succeed. Don't blindly retry (that risks a duplicate-looking payment); instead poll `GET /v1/payments` for the payment's status before deciding whether to resubmit. They shouldn't — this API is designed so paths, request fields, and response fields stay identical whether your account has migrated to Rise's new infrastructure or not (requests are transparently forwarded to the original V1 backend until migration completes). If you're seeing a real behavioral difference, treat it as a bug and contact Rise support with the request/response pair. ## HTTP status codes **Common causes:** missing/invalid required fields, an unresolvable identifier, or a value outside allowed bounds (e.g. title too long). **Response shape:** `{ "error": "Bad Request", "message": { "type": "invalid_parameters", "errors": { "": [""] } } }` **Common causes:** missing `Authorization` header, or a JWT that's expired (tokens last 24 hours). **Resolution:** re-run the [authentication flow](/v1-legacy/authentication) to get a fresh token. **Common causes:** wallet/impersonation checks failed on auth endpoints, or the authenticated user lacks the required permission/role on the target team/company. **Resolution:** see the "Every request returns 403" item above. **Common causes:** the team, user, payee, talent, or RiseAccount the identifier resolves to doesn't exist (or the identifier itself doesn't resolve to anything). **Resolution:** double check the identifier value and its [format](/v1-legacy/overview#identifier-formats). **Common causes:** a payment or payment group with the same on-chain id already exists — almost always a reused `salt`. **Resolution:** retry with a new salt. **Common causes:** an execute-payment transaction wasn't confirmed on-chain within the request window. **Resolution:** poll `GET /v1/payments` for the actual status before retrying. ## Per-endpoint error reference For the exhaustive list of error causes on a specific endpoint, see that endpoint's own "Errors" table: SIWE/JWT-specific error causes Instant pay, batch pay, and intents error causes List/get/remove talent error causes Balance lookup error causes Invite send/list error causes ## Still stuck? Reach out to [hello@riseworks.io](mailto:hello@riseworks.io) with the request you sent (minus any private key/signature) and the exact response body — the `error`/`message` fields usually pinpoint the fix immediately. # Deposit Events Source: https://docs.riseworks.io/webhooks/events/deposit Webhook events related to fund deposits and incoming transfers Deposit events are triggered when funds are received in company accounts from external sources. These events provide real-time notifications about incoming transfers and their confirmation status. ## Overview Rise supports the following deposit-related webhook events: Funds deposited to company account from external sources *** ## deposit.received When funds are deposited into a company account from an external source, this event is triggered. This includes deposits from exchanges, other wallets, or payment providers. ### Field reference The type of object this webhook represents (always "event") The Unix timestamp when the event was created The type of event that occurred The version of the event schema A unique identifier for the API request that triggered this event (optional) A unique key to ensure the webhook is processed only once Deposit information and details Primary key of the deposit (nanoid) Company or team nanoid that received the deposit ID of the entity deposit account used Unique identifier from the deposit provider Provider-specific entity identifier Provider-specific wallet identifier Name of the deposit provider (manual, routefusion, bitcoin, rise\_pay\_handler) Currency code of the source amount Source amount in cents Currency code of the destination amount Destination amount in cents (what was actually deposited) ISO-8601 timestamp of when the transaction occurred Purpose or reason for the payment Reference number or identifier Trace number for the transaction Blockchain transaction hash if applicable Blockchain reference for the deposit Bank account identifier seen in the deposit Current status of the deposit (pending, complete, failed, rejected, reverted) ISO-8601 timestamp when the deposit was created ISO-8601 timestamp when the deposit was last updated Token information for this deposit Blockchain address of the token Human-readable name of the token Token symbol (e.g., USDC, ETH) Number of decimal places for the token Blockchain network where the deposit occurred ```json deposit.received (v1) theme={null} { "object": "event", "created": 1751590453, "request_id": "req-1751590452487", "event_type": "deposit.received", "event_version": "1.0", "idempotency_key": "85420805-0b5e-4b11-b7f4-c6f05db7120b", "deposit": { "nanoid": "dp-abc123def456789", "entity_nanoid": "co-abc123def456789", "entity_deposit_account_id": 12345, "provider_deposit_id": "rf_deposit_abc123def456", "provider_entity_id": "rf_entity_xyz789", "provider_wallet_id": "rf_wallet_abc123", "provider_name": "routefusion", "source_currency": "USD", "source_amount_cents": 500000, "destination_currency": "USDC", "destination_amount_cents": 499500, "transaction_date": "2024-01-15T10:30:00.000Z", "purpose_of_payment": "Funding company operations", "reference": "REF123456789", "trace": "TR987654321", "funding_blockchain_txn": "0x1234567890abcdef1234567890abcdef12345678", "blockchain_reference": "rise_deposit_abc123def456", "deposit_bank_account_seen": 98765, "status": "complete", "created_at": "2024-01-15T10:30:00.000Z", "updated_at": "2024-01-15T10:35:00.000Z", "token": { "address": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", "name": "USD Coin", "symbol": "USDC", "decimals": 6 }, "network": "polygon" } } ``` *** ## Implementation Notes **Provider integration**: Deposits can come from multiple providers including Routefusion, manual entries, Bitcoin network, or Rise Pay Handler. Check the `provider_name` field to handle provider-specific logic. **Status handling**: Always check the `status` field before processing deposits. Only `complete` deposits should be considered final and processed. **Currency conversion**: The deposit includes both source and destination amounts in cents. Use `source_amount_cents` for the original amount and `destination_amount_cents` for what was actually deposited after fees/conversion. ### Status Values Deposit has been initiated but not yet processed or confirmed Deposit has been successfully processed and funds are available Deposit processing failed due to technical or validation errors Deposit was rejected due to compliance or business rule violations Deposit was processed but later reversed or cancelled ### Provider Types Rise supports deposits from the following providers: * **manual** - Manually entered deposits for tracking purposes * **routefusion** - Bank wire transfers and ACH deposits via Routefusion * **bitcoin** - Bitcoin network deposits * **rise\_pay\_handler** - Internal Rise payment processing system ### Network Support Rise supports deposits on the following blockchain networks: * **ethereum** - Ethereum mainnet * **arbitrum** - Arbitrum Layer 2 network * **base** - Base Layer 2 network * **polygon** - Polygon (Matic) network * **avalanche** - Avalanche C-Chain * **optimism** - Optimism Layer 2 network # Invite Events Source: https://docs.riseworks.io/webhooks/events/invite Webhook events for the full invitation lifecycle — accepted, rejected, and expired Invite events are triggered as an invitation moves through its lifecycle: accepted by the invitee, rejected by them, or left to expire. These events help you track invitation workflows and member onboarding, and let you close the loop on invitations that never convert. ## Overview Rise supports the following invite-related webhook events: Invitation accepted and user joined team or company Invitation declined by the invitee Invitation lapsed after 90 days without a response These three events cover the outcomes an **invitee** can produce: they accept, they decline, or they never respond and the invitation lapses. Some invitations end **without** a webhook: one you revoke, one superseded by a resend, one dropped when the invitee's account is deactivated, and one that is set aside during sign-up because its role does not fit the role the invitee joined as. Do not treat "no event yet" as "still pending" indefinitely — reconcile against the invites API if you need the settled state of every invitation you sent. *** ## invite.accepted When a user accepts an invitation to join a company or team, this event is triggered. This event signals successful completion of the invitation workflow. ### Field reference The type of object this webhook represents (always "event") The Unix timestamp when the event was created The type of event that occurred The version of the event schema A unique identifier for the API request that triggered this event (optional) A unique key to ensure the webhook is processed only once Invitation information and details Unique identifier for the invite (primary key) Nanoid of the user who accepted the invite Email address of the invitee Role assigned to the invitee Status of the invite, e.g., pending, accepted, expired Indicates whether the invite was sent anonymously Nanoid of the user who created the invite Nanoid of the company or team the invite is associated with *** ## invite.rejected When an invitee explicitly declines an invitation, this event is triggered. The invitation is dead and will never be accepted. ### Field reference The type of object this webhook represents (always "event") The Unix timestamp when the event was created The type of event that occurred The version of the event schema A unique identifier for the API request that triggered this event (optional) A unique key to ensure the webhook is processed only once Invitation information and details Unique identifier for the invite (primary key) Nanoid of the user who rejected the invite Email address of the invitee Role that was offered to the invitee Always `rejected` for this event Indicates whether the invite was sent anonymously Nanoid of the user who created the invite Nanoid of the company or team the invite is associated with *** ## invite.expired Invitations are valid for **90 days from the moment they are created**. An invitation that is still pending when it passes that deadline lapses, and this event is triggered. Expiry is swept periodically rather than exactly on the 90-day boundary, so expect this event shortly after the invitation lapses rather than to the second. Only invitations that were still pending expire — one that was already accepted or rejected has reached its final state and produces no `invite.expired`. Because the invitee may never have created a Rise account, this payload carries **no `user_nanoid`**. That is the only shape difference from the other two invite events. ### Field reference The type of object this webhook represents (always "event") The Unix timestamp when the event was created The type of event that occurred The version of the event schema A unique identifier for the API request that triggered this event (optional) A unique key to ensure the webhook is processed only once Invitation information and details Unique identifier for the invite (primary key) Email address of the invitee Role that was offered to the invitee Always `expired` for this event Indicates whether the invite was sent anonymously Nanoid of the user who created the invite Nanoid of the company or team the invite is associated with ISO timestamp at which the invite lapsed (90 days after it was created) ```json invite.accepted (v2) theme={null} { "object": "event", "created": 1751590453, "request_id": "req-1751590452487", "event_type": "invite.accepted", "event_version": "2.0", "idempotency_key": "85420805-0b5e-4b11-b7f4-c6f05db7120b", "invite": { "nanoid": "in-abc123def456789", "user_nanoid": "us-xyz789abc123456", "email": "newmember@example.com", "role": "team_admin", "status": "accepted", "anonymous": false, "invited_by": "us-def456ghi789012", "invited_to": "te-ghi789def456123" } } ``` ```json invite.rejected (v2) theme={null} { "object": "event", "created": 1751590453, "request_id": null, "event_type": "invite.rejected", "event_version": "2.0", "idempotency_key": "0f2c9a41-5d3b-4a77-9f1e-3c8b5d7a2e40", "invite": { "nanoid": "in-abc123def456789", "user_nanoid": "us-xyz789abc123456", "email": "newmember@example.com", "role": "team_admin", "status": "rejected", "anonymous": false, "invited_by": "us-def456ghi789012", "invited_to": "te-ghi789def456123" } } ``` ```json invite.expired (v2) theme={null} { "object": "event", "created": 1751590453, "request_id": null, "event_type": "invite.expired", "event_version": "2.0", "idempotency_key": "b7d41e08-6a92-4c15-8e33-1f9a6c2d5b74", "invite": { "nanoid": "in-abc123def456789", "email": "newmember@example.com", "role": "team_admin", "status": "expired", "anonymous": false, "invited_by": "us-def456ghi789012", "invited_to": "te-ghi789def456123", "expires_at": "2026-07-14T09:12:04.000Z" } } ``` *** ## Implementation Notes **Invitation lifecycle**: An invitee can do three things with an invitation, and each emits an event — `invite.accepted` when they join, `invite.rejected` when they decline, and `invite.expired` when they never respond and it lapses 90 days after it was created. Subscribe to all three to learn the outcome of the invitations you send. Invitations that *you* end — revoking one, resending one (which cancels the original), or deactivating the invitee's account — emit no webhook. **Role permissions**: Always validate that the assigned role matches your expected permissions before granting access to systems or resources. **User tracking**: Use the `user_nanoid` to correlate `invite.accepted` and `invite.rejected` with other user-related activities across your system. `invite.expired` has no `user_nanoid` — correlate it on the invite's `nanoid` or `email` instead. ### Role Types Full administrative access to team settings, member management, and billing Standard team member with access to team resources and functionality Read-only access to team resources without modification privileges ### Use Cases Invite events are commonly used for: * **Automated onboarding** - Trigger welcome emails, setup guides, or account provisioning * **Access control** - Grant appropriate permissions in external systems * **Analytics tracking** - Monitor team growth and invitation conversion rates * **Compliance logging** - Maintain audit trails of team membership changes * **Chasing stalled invitations** - Re-invite or clean up your own records when `invite.expired` or `invite.rejected` tells you an invitation will never convert ### Anonymous Invitations When `anonymous` is `true`, the invitation was sent to a user who wasn't previously registered with Rise. These users create an account as part of accepting the invitation. **Account creation**: Anonymous invitations trigger both account creation and team membership, so `invite.accepted` represents both user registration and team joining. An anonymous invitation that expires never produced an account, which is why `invite.expired` carries no `user_nanoid`. # Payment Events Source: https://docs.riseworks.io/webhooks/events/payment Webhook events related to payment processing and execution Payment events are triggered during various stages of the payment lifecycle, from group creation to individual payment completion. These events provide real-time updates about payment status and execution details. ## Overview Rise supports the following payment-related webhook events: Payment group created with multiple payments Individual payment processed and sent *** ## payment.group.created When a payment group is created, this event is triggered. A payment group represents a collection of payments that are processed together as a single batch. ### Field reference The type of object this webhook represents (always "event") The Unix timestamp when the event was created The type of event that occurred The version of the event schema A unique identifier for the API request that triggered this event (optional) Primary identifier for the webhook event and a unique key to ensure it is processed only once Primary key of the payment group (nanoid) Identifier of the payment group on the blockchain ISO-8601 timestamp when the payment group was created ISO-8601 timestamp when the payment group was last updated Type of invoice (e.g., "time\_entry", "flat", "recurring") Description of the work or service being paid for Role or position description for the work Currency code for the payment (e.g., "USD", "EUR") Array of individual payments within this group Primary key of the payment (nanoid) Identifier of the payment on the blockchain On-chain group ID that this payment belongs to Reference to the associated invoice nanoid Transaction nanoid of the blockchain tx that created the payment Transaction nanoid of the blockchain tx that processed the payment ISO-8601 timestamp when the payment row was created ISO-8601 timestamp when the payment row was last updated Unix timestamp (seconds) when the payment becomes executable Numeric enum value describing the on-chain payment type Token information for this payment ERC-20 token contract address Token name (e.g., "USD Coin") Token symbol (e.g., "USDC") Number of decimal places for the token Array of payment recipients Primary key of the recipient (nanoid) Email address of the recipient Name of the recipient The type of entity (user, team, company) Blockchain wallet address of the recipient (payee). Blockchain wallet address of the paying team account. Payment amount in the token smallest units The payment amount in cents (USD equivalent) *** ## payment.sent When an individual payment is processed and sent, this event is triggered. This occurs after a payment has been successfully executed on the blockchain. ### Field reference The type of object this webhook represents (always "event") The Unix timestamp when the event was created The type of event that occurred The version of the event schema A unique identifier for the API request that triggered this event (optional) A unique key to ensure the webhook is processed only once Payment information and details Primary key of the payment (nanoid) Identifier of the payment on the blockchain On-chain group ID that this payment belongs to Reference to the associated invoice nanoid Transaction nanoid of the blockchain tx that created the payment Transaction nanoid of the blockchain tx that processed the payment ISO-8601 timestamp when the payment row was created ISO-8601 timestamp when the payment row was last updated Unix timestamp (seconds) when the payment becomes executable Numeric enum value describing the on-chain payment type Token information for this payment ERC-20 token contract address Token name (e.g., "USD Coin") Token symbol (e.g., "USDC") Number of decimal places for the token Array of payment recipients Primary key of the recipient (nanoid) Email address of the recipient Name of the recipient The type of entity (user, team, company) Blockchain wallet address of the recipient (payee). Blockchain wallet address of the paying team account. Payment amount in the token smallest units Payment amount in cents (USD equivalent) Type of invoice (e.g., "time\_entry", "flat", "recurring") Description of the work or service being paid for Role or position description for the work Currency code for the payment (e.g., "USD", "EUR") Information about the payment group this payment belongs to Primary key of the payment group (nanoid) Identifier of the payment group on the blockchain ISO-8601 timestamp when the payment group was created ISO-8601 timestamp when the payment group was last updated ```json payment.group.created (v1) theme={null} { "object": "event", "created": 1751590453, "request_id": "req-1751590452487", "event_type": "payment.group.created", "event_version": "1.0", "idempotency_key": "85420805-0b5e-4b11-b7f4-c6f05db7120b", "payment_group": { "nanoid": "pg-abc123def456789", "onchain_id": "12345", "created_at": "2024-01-15T10:30:00.000Z", "updated_at": "2024-01-15T10:30:00.000Z", "invoice_type": "time_entry", "invoice_description": "Development work for Q1 2024", "role_description": "Senior Frontend Developer", "currency": "USD", "payments": [ { "nanoid": "pa-xyz789abc123456", "onchain_id": "67890", "group_onchain_id": "12345", "invoice_nanoid": "iv-def456ghi789012", "creation_transaction": "tx-abc123def456789", "process_transaction": null, "created_at": "2024-01-15T10:30:00.000Z", "updated_at": "2024-01-15T10:30:00.000Z", "pay_at_time": "1705312200", "pay_type": "1", "token": { "address": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", "name": "USD Coin", "symbol": "USDC", "decimals": 6 }, "recipients": [ { "nanoid": "pa-xyz789abc123456", "email": "contractor@example.com", "name": "John Doe", "entity_type": "user", "recipient_address": "0x24D5ad606b98A25287e393Ad0EF410F489F5E780" } ], "payer_address": "0x47448085d91b06F3A1D592e9E189B78a867594Ef", "amount": "1000000000", "amount_cents": 100000 } ] } } ``` ```json payment.sent (v1) theme={null} { "object": "event", "created": 1751590453, "request_id": "req-1751590452487", "event_type": "payment.sent", "event_version": "1.0", "idempotency_key": "85420805-0b5e-4b11-b7f4-c6f05db7120b", "payment": { "nanoid": "pa-xyz789abc123456", "onchain_id": "67890", "group_onchain_id": "12345", "invoice_nanoid": "iv-def456ghi789012", "creation_transaction": "tx-abc123def456789", "process_transaction": "tx-def456ghi789012", "created_at": "2024-01-15T10:30:00.000Z", "updated_at": "2024-01-15T10:35:00.000Z", "pay_at_time": "1705312200", "pay_type": "1", "token": { "address": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", "name": "USD Coin", "symbol": "USDC", "decimals": 6 }, "recipients": [ { "nanoid": "pa-xyz789abc123456", "email": "contractor@example.com", "name": "John Doe", "entity_type": "user", "recipient_address": "0x24D5ad606b98A25287e393Ad0EF410F489F5E780" } ], "payer_address": "0x47448085d91b06F3A1D592e9E189B78a867594Ef", "amount": "1000000000", "amount_cents": 100000, "invoice_type": "time_entry", "invoice_description": "Development work for Q1 2024", "role_description": "Senior Frontend Developer", "currency": "USD", "payment_group": { "nanoid": "pg-abc123def456789", "onchain_id": "12345", "created_at": "2024-01-15T10:30:00.000Z", "updated_at": "2024-01-15T10:30:00.000Z" } } } ``` *** ## Implementation Notes **Payment lifecycle**: Payment groups are created first (triggering `payment.group.created`), then individual payments within the group are processed and sent (triggering `payment.sent` for each). **Processing order**: Always handle `payment.group.created` before individual `payment.sent` events, as the group must exist before payments can be processed. **Idempotency**: Use the `idempotency_key` field to ensure your webhook handlers don't process the same payment multiple times. # Security Events Source: https://docs.riseworks.io/webhooks/events/security Webhook events related to security, fraud detection, and compliance monitoring Security events are triggered when potentially suspicious activities are detected or when security-related account actions occur. These events help you monitor and respond to security threats in real-time. ## Overview Rise supports the following security-related webhook events: Duplicate withdrawal account detected for security review *** ## withdraw\_account.duplicated\_detected When the system detects that a user is attempting to use a withdrawal account that's already associated with another user or has been flagged for suspicious activity, this event is triggered. This is part of Rise's fraud prevention and compliance monitoring. ### Field reference The type of object this webhook represents (always "event") The Unix timestamp when the event was created The type of event that occurred The version of the event schema A unique identifier for the API request that triggered this event (optional) A unique key to ensure the webhook is processed only once Account information for the affected user The unique identifier for the user whose account was affected The new status of the account (suspended, under\_review, etc.) ```json withdraw_account.duplicated_detected (v1) theme={null} { "object": "event", "created": 1751590453, "request_id": "req-1751590452487", "event_type": "withdraw_account.duplicated_detected", "event_version": "1.0", "idempotency_key": "85420805-0b5e-4b11-b7f4-c6f05db7120b", "account": { "user_id": "us-testWebhook1", "account_status_v2": "active" } } ``` *** ## Implementation Notes **Immediate action required**: When this event is triggered, the affected account is immediately suspended to prevent potential fraud. Manual review is required to restore access. **Compliance reporting**: These events should be logged for compliance purposes and may need to be reported to regulatory authorities depending on your jurisdiction. **User communication**: Consider implementing automated notifications to inform affected users about the account suspension and next steps for resolution. ### Account Status Values Account is temporarily suspended pending review due to security concerns Account is under investigation but still operational with limited functionality Account has been marked for attention but remains fully operational ### Security Response Workflow When this event is triggered, consider implementing the following response workflow: 1. **Immediate Response** * Log the security event for audit purposes * Notify your security team or compliance officer * Suspend any pending transactions for the affected user 2. **Investigation** * Review the duplicate account details * Check for patterns of suspicious activity * Verify user identity through additional KYC procedures 3. **Resolution** * If legitimate: Restore account access and update internal records * If fraudulent: Maintain suspension and report to authorities if required * Document the investigation outcome for future reference ### Common Triggers This event is typically triggered by: * **Bank account reuse** - Same bank account linked to multiple user profiles * **Identity overlap** - Similar personal information across different accounts * **Device fingerprinting** - Same device used for multiple account setups * **Behavioral patterns** - Similar transaction patterns suggesting coordinated activity **False positives**: Legitimate cases include family members sharing bank accounts or users creating new accounts after account issues. Always provide an appeal process for affected users. # Webhook Overview Source: https://docs.riseworks.io/webhooks/getting-started/overview What are webhooks and why use them with Rise ## What are webhooks? Webhooks are HTTP callbacks that Rise sends to your application when specific events occur in your account. Think of them as "reverse API calls" – instead of your application repeatedly asking Rise "did anything happen?", Rise proactively notifies your application the moment something interesting occurs. When an event happens in your Rise account – like a payment being received or a payment group being created – Rise can immediately send an HTTP POST request to your server with the event details. ## Why use Rise webhooks? Get instant updates when events occur in your Rise account, enabling you to build responsive applications that react immediately to changes. Instead of constantly polling Rise's API to check for updates, webhooks deliver information to you automatically, reducing server load and improving efficiency. Update your application's UI, send notifications to users, or trigger business processes the moment something happens, creating seamless experiences. Rise webhooks include robust retry logic with exponential backoff, ensuring your application receives events even if your server is temporarily unavailable. ## Common use cases Rise webhooks enable you to build powerful, responsive applications: * **Send confirmation emails** when payments are received * **Update order status** in your system when payment groups are created * **Trigger fulfillment processes** when deposits are confirmed * **Send push notifications** to mobile apps for real-time updates * **Sync data** between Rise and your internal systems * **Generate reports** or analytics when specific events occur ## How Rise webhooks work Tell Rise where to send webhook events by creating a webhook endpoint with your server's URL Select which types of events you want to receive (payment received, deposit processed, etc.) Rise sends HTTP POST requests to your endpoint when subscribed events occur Your application verifies the webhook signature and processes the event data Your endpoint responds with a 2xx status code to acknowledge receipt ## Webhook event structure Every Rise webhook event follows a consistent structure: ```json theme={null} { "object": "event", "created": 1751590453, "request_id": "req-1751590452487", "event_type": "payment.sent", "event_version": "2.0", "idempotency_key": "85420805-0b5e-4b11-b7f4-c6f05db7120b", "payment": { "nanoid": "pa-xyz789abc123456", "amount": "1000000000", "currency": "USD" // ... more event data } } ``` All monetary amounts in blockchain context are transmitted as **strings** to prevent JSON number precision issues. Timestamps are always in **UTC** using ISO-8601 format. ## Security first Rise webhooks include built-in security features to protect your integration: HTTPS recommended for production; HTTP supported for development/testing Each webhook includes a signature that proves it came from Rise Timestamps prevent old events from being replayed by attackers ## Team collaboration Rise webhooks support both company-level and team-level configurations: * **Company webhooks**: Receive events for your entire organization * **Team webhooks**: Get targeted notifications for specific team activities ## Getting started Ready to start receiving Rise webhook events? Here's what's next: Follow our step-by-step guide to set up your first webhook in under 10 minutes Learn about signature verification and securing your endpoints Explore all available events you can subscribe to Use our testing tools to validate your webhook integration Rise webhooks v2 provides enhanced reliability, better security, and more granular event control compared to our previous webhook system. If you're currently using Rise webhooks v1, check out our [migration guide](/webhooks/reference/migration) to upgrade. # Webhook Quick Start Source: https://docs.riseworks.io/webhooks/getting-started/quickstart Set up your first Rise webhook in under 10 minutes This guide will walk you through setting up your first Rise webhook in under 10 minutes. By the end, you'll have a working webhook endpoint that receives real-time notifications from Rise. ## Prerequisites Before you start, make sure you have: A Rise account with a RiseID (get one at app.rise.works) A server that can receive HTTP POST requests HTTPS recommended for production (HTTP works for development/testing) Your Rise API credentials from the dashboard ## Step 1: Create your webhook endpoint First, create an endpoint on your server to receive webhook events. Your endpoint should: * Accept POST requests * Return a `200` status code quickly * Handle the JSON payload Rise sends ```javascript Express.js theme={null} const express = require('express'); const app = express(); app.use(express.json()); app.post('/rise-webhooks', (req, res) => { const event = req.body; // Handle the event console.log('Received event:', event.event_type); console.log('Event ID:', event.id); console.log('Event data:', event); // Respond quickly with 200 res.status(200).json({ received: true }); }); app.listen(3000, () => { console.log('Webhook server running on port 3000'); }); ``` ```python Flask theme={null} from flask import Flask, request, jsonify app = Flask(__name__) @app.route('/rise-webhooks', methods=['POST']) def handle_webhook(): event = request.get_json() // Handle the event print(f"Received event: {event['event_type']}") print(f"Event ID: {event['id']}") print(f"Event data: {event}") // Respond quickly with 200 return jsonify({'received': True}), 200 if __name__ == '__main__': app.run(port=3000) ``` ## Step 2: Register your webhook in the Rise app Now you'll register your webhook endpoint using the Rise app's user interface: Open the Rise app and navigate to the Developer section, then click on **"Webhooks"** in the sidebar menu. Click **"Create Webhook"** to add a new webhook endpoint. Fill in the webhook details: * **Endpoint URL**: `https://your-server.com/rise-webhooks` * **Secret**: Enter a secure secret string for signature verification (e.g., `whsec_your_secure_random_string`) * **Description**: Optional description for your webhook * **Event Types**: Select the events you want to receive * **Team**: Choose company-level or specific team (optional) For this tutorial, select: * `payment.sent` - When a payment is successfully sent * `payment.group.created` - When a new payment group is created Click **"Create Webhook"** to save your configuration. Remember the secret you entered - you'll need it for signature verification. Keep your webhook secret secure! You'll need it for signature verification, and you can view it later in the webhook details page. ## Step 3: Test your webhook Rise provides a built-in testing feature directly in the app: Go to your webhook details page in the Rise app and click the **"Test Webhook"** button. Choose an event type to test (e.g., `payment.sent`). Click **"Send Test Event"** to deliver a test payload to your endpoint. Check your server logs to confirm you received the test event. You should see output similar to this in your server logs: ```bash theme={null} Received event: payment.sent Event ID: we-IQGikif4ja4A Event data: { id: 'we-IQGikif4ja4A', object: 'event', created: 1751590453, ... } ``` ## Step 4: Add signature verification with Rise SDK For security, verify that webhook requests actually come from Rise using the official Rise SDK: ```javascript Node.js with Rise SDK theme={null} const express = require('express'); const { WebhookValidator } = require('@riseworks/sdk'); const app = express(); // Use raw body for signature verification app.use('/rise-webhooks', express.raw({ type: 'application/json' })); app.post('/rise-webhooks', (req, res) => { const signature = req.headers['x-rise-signature']; const secret = process.env.RISE_WEBHOOK_SECRET; try { // Validate webhook using Rise SDK WebhookValidator const riseValidator = new WebhookValidator({ secret: secret, tolerance: 720 // 12 minutes tolerance }); // Option 1: validateEvent (throws on failure) let event = riseValidator.validateEvent(req.body, signature); // Option 2: validateEventSafe (returns result object) const result = riseValidator.validateEventSafe(req.body, signature); if (!result.isValid) { console.error('Validation failed:', result.error); return res.status(400).json({ error: result.error }); } event = result.event; // Signature is valid, process the event handleEvent(event); res.status(200).json({ received: true, timestamp: new Date().toISOString(), event_type: event.event_type, event_version: event.event_version, idempotency_key: event.idempotency_key }); } catch (error) { console.error('Webhook verification failed:', error.message); res.status(400).json({ error: 'Webhook processing failed', message: error.message, timestamp: new Date().toISOString() }); } }); ``` ```typescript TypeScript with Rise SDK theme={null} import express from 'express'; import { WebhookValidator, type RiseWebhookEvent } from '@riseworks/sdk'; const app = express(); // Use raw body for signature verification app.use('/rise-webhooks', express.raw({ type: 'application/json' })); app.post('/rise-webhooks', (req, res) => { const signature = req.headers['x-rise-signature'] as string; const secret = process.env.RISE_WEBHOOK_SECRET!; try { // Validate webhook using Rise SDK WebhookValidator with full type safety const riseValidator = new WebhookValidator({ secret: secret, tolerance: 720 // 12 minutes tolerance }); // Option 1: validateEvent (throws on failure) let event = riseValidator.validateEvent(req.body, signature); // Option 2: validateEventSafe (returns result object) const result = riseValidator.validateEventSafe(req.body, signature); if (!result.isValid) { console.error('Validation failed:', result.error); return res.status(400).json({ error: result.error }); } event = result.event; // Signature is valid, process the event with full TypeScript support handleEvent(event); res.status(200).json({ received: true, timestamp: new Date().toISOString(), event_type: event.event_type, event_version: event.event_version, idempotency_key: event.idempotency_key }); } catch (error) { console.error('Webhook verification failed:', error instanceof Error ? error.message : String(error)); res.status(400).json({ error: 'Webhook processing failed', message: error instanceof Error ? error.message : String(error), timestamp: new Date().toISOString() }); } }); ``` ```python Python (Manual implementation) theme={null} import hmac import hashlib import time import json def verify_rise_signature(payload, signature, secret): # Parse the signature header elements = signature.split(',') timestamp = int(elements[0].split('=')[1]) received_hash = elements[1].split('=')[1] # Create the signed payload signed_payload = f"{timestamp}.{json.dumps(payload, separators=(',', ':'))}" # Generate expected signature expected_hash = hmac.new( secret.encode('utf-8'), signed_payload.encode('utf-8'), hashlib.sha256 ).hexdigest() # Verify timestamp (prevent replay attacks) current_time = int(time.time()) tolerance = 600 # 10 minutes if current_time - timestamp > tolerance: raise ValueError('Timestamp too old') # Verify signature if not hmac.compare_digest(received_hash, expected_hash): raise ValueError('Invalid signature') return True @app.route('/rise-webhooks', methods=['POST']) def handle_webhook(): signature = request.headers.get('X-Rise-Signature') secret = os.environ.get('RISE_WEBHOOK_SECRET') try: verify_rise_signature(request.get_json(), signature, secret) # Signature is valid, process the event event = request.get_json() handle_webhook_event(event) return jsonify({'received': True}), 200 except ValueError as e: print(f'Webhook verification failed: {e}') return 'Invalid signature', 400 ``` Use the exact same secret string you entered when creating the webhook. You can view it in your webhook details page in the Rise app if needed. ### Installing the Rise SDK To use the Rise SDK for webhook validation, install it in your project: ```bash theme={null} # Using npm npm install @riseworks/sdk # Using yarn yarn add @riseworks/sdk # Using pnpm pnpm add @riseworks/sdk ``` The SDK provides: * **Class-based webhook validation** using `WebhookValidator` * **Type-safe webhook validation** with full TypeScript support * **Automatic signature verification** with configurable tolerance * **Proper error handling** with detailed error messages * **Support for multiple input types** (string, Buffer, object) ## Step 5: Handle events in your application Now you can add business logic to handle specific events with full type safety: ```typescript theme={null} import type { RiseWebhookEvent, PaymentSentV1, PaymentGroupCreatedV1, DepositReceivedV1 } from '@riseworks/sdk'; function handleEvent(event: RiseWebhookEvent) { switch (event.event_type) { case 'payment.sent': // Send confirmation email with full type safety sendPaymentConfirmation(event as PaymentSentV1); break; case 'payment.group.created': // Update order status with full type safety updateOrderStatus(event as PaymentGroupCreatedV1); break; case 'deposit.received': // Trigger fulfillment with full type safety triggerFulfillment(event as DepositReceivedV1); break; default: console.log('Unhandled event type:', event.event_type); } } async function sendPaymentConfirmation(event: PaymentSentV1) { // Your email logic here with full type safety console.log(`Sending confirmation for payment ${event.payment.nanoid}`); console.log(`Amount: ${event.payment.amount_cents} cents`); console.log(`Recipients: ${event.payment.recipients.length}`); } async function updateOrderStatus(event: PaymentGroupCreatedV1) { // Your database update logic here with full type safety console.log(`Updating payment group ${event.payment_group.nanoid} to processing`); console.log(`Payments count: ${event.payment_group.payments?.length || 0}`); } async function triggerFulfillment(event: DepositReceivedV1) { // Your fulfillment logic here with full type safety console.log(`Triggering fulfillment for deposit ${event.deposit.nanoid}`); console.log(`Amount: ${event.deposit.destination_amount_cents} cents`); } ``` ```javascript theme={null} // JavaScript version (without TypeScript types) function handleEvent(event) { switch (event.event_type) { case 'payment.sent': // Send confirmation email sendPaymentConfirmation(event); break; case 'payment.group.created': // Update order status updateOrderStatus(event); break; case 'deposit.received': // Trigger fulfillment triggerFulfillment(event); break; default: console.log('Unhandled event type:', event.event_type); } } async function sendPaymentConfirmation(event) { // Your email logic here console.log(`Sending confirmation for payment ${event.payment.nanoid}`); } async function updateOrderStatus(event) { // Your database update logic here console.log(`Updating payment group ${event.payment_group.nanoid} to processing`); } ``` ## Common event types to start with Here are the most commonly used Rise webhook events: A payment has been successfully sent to the recipient A new payment group has been created with multiple payments A deposit has been received and confirmed in your account A team member has accepted an invitation to join An invitee has declined an invitation An invitation lapsed after 90 days without a response ## Testing checklist Before going live, verify that your webhook: * Responds with a `200` status code within 10 seconds * Handles all the event types you've subscribed to * Processes events idempotently (handles duplicates gracefully) * Logs events for debugging * Verifies webhook signatures correctly (use Rise SDK for best results) * Rejects invalid signatures properly * Validates timestamps to prevent replay attacks * Uses HTTPS for production (recommended) * Handles Buffer payloads correctly for signature verification * Handles unknown event types gracefully * Doesn't crash on malformed payloads * Has proper error logging and monitoring ## Monitor your webhook deliveries Once your webhook is active, you can track its performance in the Rise app: 1. **Navigate to your webhook details page** in the Rise app 2. **Click on "Delivery History"** tab to see all webhook deliveries 3. **Monitor delivery success rates** and response times 4. **Review failed deliveries** to identify and fix issues The delivery history shows: * **Successful deliveries** - Events delivered with 200 response * **Failed deliveries** - Events that couldn't be delivered * **Pending retries** - Events waiting to be retried * **Response details** - HTTP status codes and response times ## What's next? Congratulations! You now have a working webhook endpoint. Here's what to explore next: See the complete list of available events and their payloads Learn about advanced security features and best practices Learn how to manage multiple webhooks and team configurations Testing your webhooks locally and troubleshooting common issues **Pro tip**: Start with just one or two event types to keep things simple, then add more as your integration grows. It's easier to debug and maintain fewer, well-configured webhooks. **SDK recommendation**: Use the Rise SDK for webhook validation to get automatic type safety, proper error handling, and protection against common security issues. The SDK handles Buffer payloads, signature verification, and timestamp validation automatically. # Security and Verification Source: https://docs.riseworks.io/webhooks/getting-started/security How to verify webhook authenticity and secure your endpoints Webhook security is critical for protecting your application from malicious attacks and ensuring data integrity. This guide covers everything you need to implement robust security for your Rise webhook endpoints. ## Why webhook security matters Without proper security measures, attackers could: Trigger unauthorized actions in your system with fabricated webhook events Duplicate transactions or operations by resending legitimate webhook data Access sensitive payment information transmitted over insecure connections Launch denial-of-service attacks against your webhook endpoints ## Security architecture overview Rise webhooks include multiple layers of security to protect your integration: All data encrypted in transit between Rise and your endpoint Cryptographic proof that requests actually came from Rise Protection against replay attacks using time-based verification Secure storage and handling of webhook secrets ## Signature verification Every Rise webhook includes a cryptographic signature that proves the request came from Rise. **SDK Installation**: Install the Rise SDK to get secure, type-safe webhook verification: `npm install @riseworks/sdk` (or `yarn add @riseworks/sdk` / `pnpm add @riseworks/sdk`) ### How signatures work 1. **Rise generates signature** using your webhook secret and event data 2. **Signature included in headers** with timestamp information 3. **Your endpoint verifies** the signature matches expected value 4. **Process event** only if signature is valid ### Signature header format Rise includes the signature in the `X-Rise-Signature` header: ``` X-Rise-Signature: t=1640995200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd ``` * `t=` - Timestamp when the signature was generated * `v1=` - The signature hash using HMAC-SHA256 ### Implementing signature verification The Rise SDK provides a secure, type-safe way to verify webhook signatures. Here's how to use it: ```javascript Node.js theme={null} import { WebhookValidator } from '@riseworks/sdk'; // Create validator instance with your webhook secret const validator = new WebhookValidator({ secret: process.env.RISE_WEBHOOK_SECRET, tolerance: 600 // 10 minutes (default) }); // Express.js middleware example app.use('/rise-webhooks', express.raw({ type: 'application/json' })); app.post('/rise-webhooks', async (req, res) => { const signature = req.headers['x-rise-signature']; try { // Verify signature and get validated event (uses default tolerance) const event = validator.validateEvent(req.body, signature); // For high-security scenarios, you can override tolerance: // const event = validator.validateEvent(req.body, signature, { tolerance: 60 }); // Event is now verified and typed - process it handleWebhookEvent(event); res.status(200).json({ received: true }); } catch (error) { console.error('Webhook verification failed:', error.message); res.status(400).send('Invalid signature'); } }); ``` ```python Python theme={null} from riseworks import WebhookValidator # Create validator instance validator = WebhookValidator( secret=os.environ.get('RISE_WEBHOOK_SECRET'), tolerance=600 # 10 minutes ) # Flask example @app.route('/rise-webhooks', methods=['POST']) def handle_webhook(): signature = request.headers.get('X-Rise-Signature') try: # Verify signature and get validated event event = validator.validateEvent(request.get_data(), signature) # Event is now verified and typed - process it handle_webhook_event(event) return jsonify({'received': True}), 200 except Exception as e: print(f'Webhook verification failed: {e}') return 'Invalid signature', 400 ``` **SDK Benefits**: The Rise SDK handles all signature verification, timestamp validation, and type safety automatically. It also provides TypeScript types for all webhook events. ## HTTPS recommendations HTTPS is recommended for production webhook endpoints to protect data in transit, though Rise supports both HTTP and HTTPS endpoints. Rise webhooks work with both HTTP and HTTPS endpoints. For production environments handling sensitive financial data, HTTPS is recommended for security. HTTP endpoints are perfectly suitable for development, testing, and localhost environments. ### Why HTTPS is recommended Prevents interception of sensitive payment data during transmission Ensures you're communicating with Rise's legitimate servers Prevents tampering with webhook data during transmission Required for PCI DSS and financial data protection standards For production environments, HTTPS with a valid SSL certificate is recommended. For development and testing, HTTP endpoints (including localhost and ngrok) work perfectly fine. Use the Rise app's test feature to verify your endpoint is properly configured. ## Preventing replay attacks Replay attacks occur when an attacker intercepts a valid webhook and sends it again later. ### How replay protection works 1. **Rise includes timestamp** in every webhook signature 2. **Your endpoint checks timestamp** against current time 3. **Reject old requests** outside tolerance window 4. **Log suspicious activity** for monitoring ### Timestamp validation with SDK The Rise SDK automatically handles timestamp validation with configurable tolerance. You can set a default tolerance in the constructor and override it per validation call: ```javascript theme={null} import { WebhookValidator } from '@riseworks/sdk'; // Create validator with default tolerance (10 minutes) const validator = new WebhookValidator({ secret: process.env.RISE_WEBHOOK_SECRET, tolerance: 600 // 10 minutes (default) }); // Use default tolerance const event = validator.validateEvent(req.body, signature); // Override tolerance for high-security scenarios const strictEvent = validator.validateEvent(req.body, signature, { tolerance: 60 // 1 minute for high security }); // Override tolerance for systems with clock sync issues const lenientEvent = validator.validateEvent(req.body, signature, { tolerance: 900 // 15 minutes for clock sync issues }); ``` ### Choosing tolerance windows **Recommended for most applications** Provides good security while allowing for reasonable network delays and clock differences between servers. **High security environments** Maximum security but may cause issues if your server has clock synchronization problems or high network latency. **Systems with clock issues** For systems with known clock synchronization issues, but reduces replay attack protection. Don't set tolerance to 0 seconds - this disables replay protection entirely and may cause legitimate webhooks to fail. ## Secret management Webhook secrets are critical security credentials that require careful handling. ### Secret storage best practices Never hardcode secrets in source code - use environment variables instead Use AWS Secrets Manager, HashiCorp Vault, or similar secure storage systems Encrypt secret storage and limit access to authorized personnel only Implement role-based access control for viewing and modifying secrets ## Common security pitfalls Avoid these common webhook security mistakes: ```javascript theme={null} // WRONG: Skipping signature verification app.post('/webhooks', (req, res) => { // Processing without verification - DANGEROUS! handleEvent(req.body); res.status(200).send('OK'); }); // WRONG: Using HTTP in production const webhookUrl = 'http://mysite.com/webhooks'; // Insecure! // WRONG: Hardcoded secrets const secret = 'whsec_abc123'; // Never do this! // WRONG: No timestamp validation function verifySignature(payload, signature, secret) { // Missing timestamp check allows replays return crypto.createHmac('sha256', secret) .update(JSON.stringify(payload)) .digest('hex') === signature; } ``` ```javascript theme={null} // CORRECT: Using Rise SDK for security import { WebhookValidator } from '@riseworks/sdk'; const validator = new WebhookValidator({ secret: process.env.RISE_WEBHOOK_SECRET, tolerance: 600 }); app.post('/webhooks', async (req, res) => { try { // SDK handles all verification automatically (uses default tolerance) const event = validator.validateEvent(req.body, req.headers['x-rise-signature']); // For high-security scenarios, override tolerance per request: // const event = validator.validateEvent(req.body, req.headers['x-rise-signature'], { tolerance: 60 }); // Process only verified events with full type safety handleEvent(event); res.status(200).json({ received: true }); } catch (error) { res.status(400).send('Verification failed'); } }); // CORRECT: HTTPS endpoint const webhookUrl = 'https://mysite.com/webhooks'; // CORRECT: Environment-based secrets const secret = process.env.RISE_WEBHOOK_SECRET; ``` ## Testing your security implementation Use the Rise app's test webhook feature to verify your security implementation: Confirm your endpoint properly processes legitimate webhook events Verify your endpoint correctly rejects webhooks with bad signatures Check the delivery history for any security-related errors or failures ## Security checklist Before deploying to production, verify your implementation: * Webhook signature is verified for every request * Invalid signatures are rejected with 400 status * Secret is stored securely (not hardcoded) * HMAC comparison uses constant-time comparison * Timestamp is checked against current time * Appropriate tolerance window is configured * Future timestamps are rejected * Replay attacks are prevented * HTTPS is recommended for production * Valid SSL certificate is installed * HTTP redirects to HTTPS (if applicable) * Strong TLS configuration * Security errors are logged appropriately * Sensitive data is not leaked in error messages * Rate limiting is implemented if needed * Monitoring alerts are configured ## What's next? Now that you've secured your webhook endpoints: Learn how to create, update, and manage your webhook endpoints Explore all available events and their data structures Testing your webhooks locally and troubleshooting common issues Track security metrics and delivery performance **Security reminder**: Webhook security is not optional. Always implement signature verification and consider HTTPS for production. The few extra lines of code can prevent serious security breaches. # Event Types Overview Source: https://docs.riseworks.io/webhooks/implementation/event-types Complete reference of all available events and their data structures Understanding webhook event payloads is crucial for building robust integrations with Rise. This guide provides comprehensive documentation for all event types, their data structures, and field specifications. ## Understanding event payloads Every webhook event follows a consistent structure with three main components: ### Event envelope All webhook events are wrapped in a standard envelope: ```json Example Event Structure theme={null} { "object": "event", "created": 1751590453, "request_id": "req-1751590452487", "event_type": "payment.sent", "event_version": "2.0", "idempotency_key": "85420805-0b5e-4b11-b7f4-c6f05db7120b", // Event-specific data goes here } ``` Consistent metadata present in all events for tracking and processing Event-specific data that varies by event type and contains the actual business data ### Envelope field reference The type of object this webhook represents (always "event") The Unix timestamp when the event was created The type of event (e.g., `payment.sent`) Schema version for the event — see [Event versions](#event-versions) Request tracking identifier Key to prevent duplicate processing ### Event versions `event_version` is the version of that event's payload schema. Each event type is versioned independently, so two events delivered to the same endpoint can be on different versions. **Versions start at `2.0`, not `1.0`.** Version `1.0` belongs to the legacy v1 webhooks, whose payloads have a different shape and signing scheme entirely. To keep the two from being confused, every event on the current v2 webhooks starts at `2.0` — including events introduced long after v2 shipped, such as `invite.expired` and `invite.rejected`. A `2.0` therefore does not mean an event has been revised once; it means it is the first version of that event on v2. From there, an event's version moves independently as its payload changes: | Change | Version | Example | | ---------------------------------------------------- | ------------- | -------------------------------------- | | First version of an event on v2 webhooks | `2.0` | `invite.expired` | | Additive change (new optional field) | `2.0` → `2.1` | — | | Breaking change (field removed, renamed, or retyped) | `2.x` → `3.0` | `withdraw_account.duplicated_detected` | Always branch on `event_type` **and** `event_version` when you handle an event. A new version of an event you already consume can change the payload, and reading `event_version` is how you notice. If you are receiving `"event_version": "1.0"`, you are on a **legacy v1 webhook endpoint**, not a v2 one. The payload shape you receive is the legacy one, and the new events documented here are not delivered to it. See the [migration guide](/webhooks/reference/migration). ### Data types and formats Rise webhooks use consistent data types across all events: * **Nanoids** - 15-character identifiers with prefixes (e.g., `co-abc123def456789`) * **ISO-8601 timestamps** - UTC timestamps (e.g., `2024-01-15T10:30:00.000Z`) * **Email addresses** - Valid email format * **Currency codes** - 3-letter ISO codes (e.g., `USD`, `EUR`) * **Blockchain addresses** - 42-character hex addresses (e.g., `0x1234...`) * **Amounts** - Large integers as strings to avoid precision loss * **Cents** - Integer values for fiat currency amounts * **Decimals** - Integer values for token decimal places **Important**: All monetary amounts in blockchain context are transmitted as **strings** to prevent JSON number precision issues. Fiat amounts are typically in **cents** as integers. ## Complete event example Here's how a complete webhook event looks when delivered to your endpoint: ```json Complete Webhook Event theme={null} { "object": "event", "created": 1751590453, "request_id": "req-1751590452487", "event_type": "payment.sent", "event_version": "2.0", "idempotency_key": "85420805-0b5e-4b11-b7f4-c6f05db7120b", "payment": { "nanoid": "pa-xyz789abc123456", "onchain_id": "67890", "group_onchain_id": "12345", "invoice_nanoid": "iv-def456ghi789012", "creation_transaction": "tx-abc123def456789", "process_transaction": "tx-def456ghi789012", "created_at": "2024-01-15T10:30:00.000Z", "updated_at": "2024-01-15T10:35:00.000Z", "pay_at_time": "1705312200", "pay_type": "1", "token": { "address": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", "name": "USD Coin", "symbol": "USDC", "decimals": 6 }, "recipients": [ { "nanoid": "pa-xyz789abc123456", "email": "contractor@example.com", "name": "John Doe", "entity_type": "user" } ], "amount": "1000000000", "amount_cents": 100000, "invoice_type": "time_entry", "invoice_description": "Development work for Q1 2024", "role_description": "Senior Frontend Developer", "currency": "USD", "payment_group": { "nanoid": "pg-abc123def456789", "onchain_id": "12345", "created_at": "2024-01-15T10:30:00.000Z", "updated_at": "2024-01-15T10:30:00.000Z" } } } ``` This example shows the complete structure with envelope fields at the top level and event-specific data flattened into the root object. *** ## Event Categories Rise webhooks are organized into the following categories based on their purpose and context: Events related to payment processing, including payment group creation and individual payment completion Events triggered when funds are deposited into company accounts from external sources Events covering the full invitation lifecycle — acceptance, rejection, and expiry Events related to account security, fraud detection, and compliance monitoring *** ## Quick Reference * **payment.group.created** - Payment group created with multiple payments * **payment.sent** - Individual payment processed and sent * **deposit.received** - Funds deposited to company account * **invite.accepted** - Team member invitation accepted * **invite.rejected** - Team member invitation declined by the invitee * **invite.expired** - Team member invitation lapsed after 90 days without a response * **withdraw\_account.duplicated\_detected** - Duplicate withdrawal account detected *** ## Implementation Tips **Field reference tip**: Each event category page contains complete field documentation and payload examples. Bookmark the relevant category pages for quick reference while building your webhook handlers. **Getting started**: If you're building a payment integration, start with [Payment Events](/webhooks/events/payment). For wallet funding notifications, check [Deposit Events](/webhooks/events/deposit). # Webhook Management Source: https://docs.riseworks.io/webhooks/implementation/management Creating, updating, and managing your webhook endpoints Once you've set up your first webhook, you'll need to manage your webhook endpoints as your integration grows. The Rise app provides a comprehensive webhook management interface where you can create, update, monitor, and deactivate your webhooks. ## Accessing webhook management To manage your webhooks: Navigate to your Rise dashboard at [app.rise.com](https://app.rise.com) Click on the **Developer** section in the main navigation Click on **"Webhooks"** in the sidebar menu ## Webhook overview page The webhooks page shows all your webhook endpoints in an organized list. Each webhook displays: The destination where events are sent Active/Inactive status with visual indicators Number of subscribed event types Last delivery time and success rate statistics ## Creating a new webhook To create a new webhook endpoint: Click the **"Create Webhook"** button on the webhooks overview page Fill out the webhook form with: * **Endpoint URL**: Your server's webhook endpoint (HTTPS recommended for production) * **Secret**: A secure string for signature verification * **Team**: Select company-level or specific team (optional) Choose which events you want to receive: * Browse available events by category * Search for specific event types * Use "Select All" for comprehensive coverage Set optional delivery parameters: * **Active**: Enable/disable the webhook immediately * **Retry policy**: Customize retry attempts (uses defaults if not specified) * **Timeout**: Set request timeout (uses system default if not specified) Click **"Create Webhook"** to save your configuration Make sure to save your webhook secret securely - you'll need it for signature verification in your code. ## Editing an existing webhook To modify a webhook configuration: Click on any webhook from the list to open its details page Click the **"Edit Webhook"** button to modify settings Modify any fields you want to change: * Change the endpoint URL * Update the secret key * Modify event subscriptions * Update team assignment Click **"Save Changes"** to apply your updates If you change the webhook secret, make sure to update your server code with the new secret before saving, or webhook deliveries will start failing. ## Managing event subscriptions You can easily modify which events a webhook receives: ### Adding new events 1. Open webhook details and go to the **"Events"** tab 2. Click **"Add Events"** button 3. Select additional event types from the available list 4. Save your changes * Use **"Select All"** to subscribe to all available events * Use category filters to add all events from specific categories * Search functionality to quickly find specific event types ### Removing events Go to webhook details and click the **"Events"** tab Click the **X** button next to any events you want to unsubscribe from Use **"Clear All"** to remove all event subscriptions, or select multiple events for batch removal ## Webhook status management Control when your webhooks are active or inactive: ### Deactivating webhooks Useful for maintenance or debugging: 1. **Toggle the status** to deactivate the webhook 2. **Perform your maintenance** work on your endpoint 3. **Reactivate when ready** by toggling status back to active Events that occur while a webhook is deactivated are not queued - they will be lost. Reactivate your webhook as soon as maintenance is complete. For webhooks no longer needed: 1. **Deactivate the webhook** using the status toggle 2. **Review delivery history** if needed for debugging 3. **Delete the webhook** if it will never be used again Permanently deactivated webhooks will not receive any events. Consider keeping them temporarily deactivated if you might need them again. ## Webhook security settings ### Updating webhook secrets Go to webhook details and find the security section Click **"Update Secret"** to generate or enter a new webhook secret **Before saving**, update your server code with the new secret Use the test webhook feature to verify the new secret works correctly Only save the new secret after confirming your code works with it ### Viewing webhook details The webhook details panel shows: Webhook ID, creation date, current settings, and security configuration Recent delivery statistics and success rates Secret management, signature verification status, and HTTPS usage Recent delivery attempts, failures, and retry information ## Webhook testing Test your webhooks without waiting for real events: 1. Go to webhook details page 2. Click **"Test Webhook"** button 3. Select an event type to simulate 4. Click **"Send Test Event"** 5. Check your server logs to confirm receipt * **Choose specific versions** if multiple event versions are available * **Test different event types** to verify your handler works for all subscribed events * **Check delivery history** for any errors or issues ## Monitoring webhook performance Keep track of your webhook health with built-in monitoring: ### Key metrics Percentage of deliveries that succeed on the first attempt Average time your endpoint takes to respond to webhook requests Percentage of deliveries that require retry attempts ### Delivery history Access detailed delivery logs to track performance: * **Successful deliveries** - Events delivered with 200 response * **Failed deliveries** - Events that couldn't be delivered * **Pending retries** - Events waiting to be retried * **Response details** - HTTP status codes and response times ## Deleting webhooks To permanently remove a webhook endpoint: Open the webhook you want to delete Click the **"Delete Webhook"** button (usually in the danger zone) Type the webhook name or confirm deletion in the dialog **Warning**: Deleting a webhook cannot be undone. All delivery history will be permanently lost. Consider deactivating instead if you might need the webhook again. ## Team-level webhook management For organizations using teams, Rise supports different webhook scopes: **Scope**: Entire company * Receive events for all company activities * Managed by company administrators * Visible to all team members (with appropriate permissions) * Best for: Company-wide integrations, reporting, analytics Company-level webhooks are ideal for integrations that need visibility into all organizational activity. **Scope**: Individual teams * Only receive events for specific team activities * Managed by team members with webhook permissions * Isolated from other team webhooks * Best for: Team-specific workflows, departmental tools Team webhooks provide granular control and help organize webhook management in larger organizations. ## Best practices for webhook management ### Organization strategies **Why it matters**: As your integration grows, you'll likely have multiple webhooks for different purposes. **Best practices**: * Use clear descriptions like "Order confirmation emails to customers" * Include the purpose: "Inventory sync to warehouse system" * Add environment indicators: "Production payment processing" **Example**: Instead of "Webhook 1", use "Customer email notifications - Production" **Organize webhooks by business function**: * **Customer-facing**: Payment confirmations, order updates, account notifications * **Internal systems**: Analytics, reporting, inventory management * **Third-party integrations**: CRM sync, accounting software, marketing tools * **Development**: Testing, staging, debugging webhooks **Quarterly review process**: * Remove unused webhooks from discontinued features * Update descriptions for changed purposes * Review and rotate webhook secrets * Clean up test/development webhooks * Monitor performance trends ### Security practices Regularly rotate webhook secrets, especially after team member changes or security incidents Track who has access to webhook management and review permissions periodically HTTPS recommended for production endpoints; verify SSL certificate validity when using HTTPS Watch for unusual patterns in delivery failures or response times ### Performance optimization **Principle**: Only subscribe to events you actually need **Benefits**: * Reduces server load and bandwidth usage * Simplifies debugging and monitoring * Lowers costs if you pay for webhook processing * Easier to maintain and understand **Tip**: Start with minimal events and add more as needed rather than subscribing to everything upfront. **Target**: Respond within 5 seconds, ideally under 2 seconds **Strategies**: * Return 200 status immediately upon receipt * Queue heavy processing for background jobs * Optimize database queries and external API calls * Monitor response time trends **Design for resilience**: * Implement idempotent event processing * Handle duplicate events safely * Log failures for investigation * Set up alerting for high failure rates ## Troubleshooting common issues ### Webhook not receiving events Ensure the webhook is active (not deactivated) in the Rise app Confirm you're subscribed to the event types you expect to receive Check if Rise is attempting deliveries but they're failing Use the built-in test feature to send a test event immediately ### High failure rates * Verify your server is accessible from external networks * Check firewall settings and port availability * Ensure your endpoint URL is correct and responds to POST requests * Test SSL certificate validity for HTTPS endpoints * Optimize slow database queries or external API calls * Move heavy processing to background queues * Increase server resources if needed * Monitor response times and set up alerting * Check server logs for errors during webhook processing * Verify signature verification logic is correct * Test with different event types to isolate issues * Use the webhook test feature to debug specific problems ## What's next? Now that you can manage webhooks effectively: Explore all available events and their complete data structures Learn advanced testing techniques for development and staging Understand how Rise delivers webhooks and handles retries Deep dive into performance tracking and troubleshooting **Pro tip**: Start with fewer, well-configured webhooks and gradually add more as your integration grows. It's easier to manage and debug a smaller number of properly configured webhooks than many poorly configured ones. # Testing Webhooks Source: https://docs.riseworks.io/webhooks/implementation/testing How to test your webhook endpoints during development Testing webhooks can be challenging since they're triggered by external events. This guide provides comprehensive strategies for testing your webhook implementations during development, staging, and production phases. ## Testing strategies overview Effective webhook testing involves multiple approaches: Test on your development machine with tunneling tools Test in production-like environment before going live Verify real-world performance with actual events Include webhooks in your continuous integration pipeline ## Local development testing **SDK Installation**: Install the Rise SDK for secure webhook testing: `npm install @riseworks/sdk` (or `yarn add @riseworks/sdk` / `pnpm add @riseworks/sdk`) ### Setting up a local webhook endpoint First, create a simple webhook endpoint for testing: ```javascript test-webhook.js theme={null} import express from 'express'; import { WebhookValidator } from '@riseworks/sdk'; const app = express(); app.use(express.raw({ type: 'application/json' })); // Create validator with test secret const validator = new WebhookValidator({ secret: process.env.RISE_WEBHOOK_SECRET || 'test-secret', tolerance: 600 // 10 minutes }); app.post('/webhook', async (req, res) => { const signature = req.headers['x-rise-signature']; console.log('📨 Webhook received:'); console.log('Headers:', req.headers); console.log('Body:', req.body.toString()); try { // Verify signature and get validated event const event = validator.validateEvent(req.body, signature); console.log('Success: Signature valid'); console.log('Event details:'); console.log('- Type:', event.event_type); console.log('- ID:', event.id); console.log('- Created:', new Date(event.created * 1000)); res.status(200).json({ received: true }); } catch (error) { console.error('Error: Error processing webhook:', error.message); res.status(400).send('Error processing webhook'); } }); const port = process.env.PORT || 3000; app.listen(port, () => { console.log(`Webhook test server running on port ${port}`); console.log(`Webhook URL: http://localhost:${port}/webhook`); }); ``` ```python test_webhook.py theme={null} from flask import Flask, request, jsonify from riseworks import WebhookValidator import os from datetime import datetime app = Flask(__name__) # Create validator with test secret validator = WebhookValidator( secret=os.environ.get('RISE_WEBHOOK_SECRET', 'test-secret'), tolerance=600 # 10 minutes ) @app.route('/webhook', methods=['POST']) def handle_webhook(): signature = request.headers.get('X-Rise-Signature') print('📨 Webhook received:') print('Headers:', dict(request.headers)) print('Body:', request.get_data().decode()) try: # Verify signature and get validated event event = validator.validateEvent(request.get_data(), signature) print('Success: Signature valid') print('Event details:') print(f'- Type: {event["event_type"]}') print(f'- ID: {event["id"]}') print(f'- Created: {datetime.fromtimestamp(event["created"])}') return jsonify({'received': True}), 200 except Exception as error: print(f'Error: Error processing webhook: {error}') return 'Error processing webhook', 400 if __name__ == '__main__': port = int(os.environ.get('PORT', 3000)) print(f'Webhook test server running on port {port}') print(f'Webhook URL: http://localhost:{port}/webhook') app.run(port=port, debug=True) ``` ### Making your local endpoint accessible Use tunneling tools to expose your local server to the internet: **Installation and usage:** ```bash theme={null} # Install ngrok npm install -g ngrok # Start your webhook server node test-webhook.js # In another terminal, expose it ngrok http 3000 ``` **Benefits:** * Reliable and stable tunnels * HTTPS support included * Custom subdomain options (paid plans) * Web interface for monitoring requests **Example output:** ``` Forwarding https://abc123.ngrok.io -> http://localhost:3000 ``` **Installation and usage:** ```bash theme={null} # Install localtunnel npm install -g localtunnel # Start your webhook server node test-webhook.js # In another terminal, create tunnel lt --port 3000 --subdomain my-webhook-test ``` **Benefits:** * Free and open source * Custom subdomain support * Simple setup **Note:** Less reliable than ngrok for production testing **Installation and usage:** ```bash theme={null} # Install cloudflared # Download from https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/ # Start tunnel cloudflared tunnel --url http://localhost:3000 ``` **Benefits:** * Built-in DDoS protection * Global CDN * No bandwidth limits **Requirements:** Cloudflare account recommended for custom domains ## Using Rise's test webhook feature The Rise app includes a built-in webhook testing tool: Navigate to your webhook details page in the Rise app and click the **"Test Webhook"** button Choose an event type to simulate from the dropdown list (e.g., `payment.sent`, `payment.group.created`) If multiple event versions are available, select the version you want to test Click **"Send Test Event"** to deliver a test payload to your endpoint Check both the Rise app interface and your server logs to confirm the test was successful ### Interpreting test results The test interface shows: HTTP status code returned by your endpoint (should be 200-299) How long your endpoint took to respond (should be under 10 seconds) What your endpoint returned (helpful for debugging) Any connection or processing errors that occurred ## Testing checklist Before deploying your webhook implementation, verify all these aspects: ### Functionality tests * Success: Webhook receives and processes events correctly * Success: Returns 200 status code within 10 seconds * Success: Handles all subscribed event types properly * Success: Logs events appropriately for debugging * Success: Processes business logic correctly * Success: Signature verification works for valid signatures * Success: Invalid signatures are properly rejected with 400 status * Success: Timestamp validation prevents replay attacks * Success: HTTPS is recommended for production environments * Success: Webhook secrets are stored securely * Success: Unknown event types are handled gracefully * Success: Malformed payloads don't crash the application * Success: Network timeouts are handled appropriately * Success: Database errors don't fail webhook processing * Success: External API failures are handled gracefully ### Idempotency testing Test that your webhook can safely handle duplicate events using the `idempotency_key` field: ```javascript theme={null} // Test idempotency with duplicate events async function testIdempotency() { const testEvent = { id: 'we-test123', object: 'event', created: Math.floor(Date.now() / 1000), event_type: 'payment.sent', event_version: '1.0', request_id: 'req_test789', idempotency_key: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', // UUID for duplicate detection payment: { nanoid: 'pa-test123', amount: '1000000', currency: 'USD', // ... other fields } }; // Send the same event multiple times for (let i = 0; i < 3; i++) { const response = await fetch('http://localhost:3000/webhook', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Rise-Signature': generateTestSignature(testEvent) }, body: JSON.stringify(testEvent) }); console.log(`Attempt ${i + 1}: ${response.status}`); } // Verify only one payment was processed using idempotency_key const processedEvents = await database.count('processed_webhooks', { idempotency_key: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' }); console.log(`Event processed ${processedEvents} times (should be 1)`); } // Example idempotency handler implementation function processWebhook(event) { const idempotencyKey = event.idempotency_key; // Check if already processed using idempotency_key const existingRecord = database.findByIdempotencyKey(idempotencyKey); if (existingRecord) { console.log(`Event ${idempotencyKey} already processed, skipping`); return { processed: false, status: "duplicate" }; } // Process the event database.saveEvent({ idempotency_key: idempotencyKey, event_type: event.event_type, processed_at: new Date(), event_data: event }); // Trigger business logic handleBusinessLogic(event); return { processed: true, status: "success" }; } ``` ## Monitoring test webhook deliveries Once your webhook is active, track its performance in the Rise app: Go to your webhook details page in the Rise app Click on the **"Delivery History"** tab to see all webhook deliveries Track delivery success rates, response times, and error patterns Configure monitoring alerts for high failure rates or slow response times ### Key metrics to track **Target**: >99% Percentage of successful first-attempt deliveries **Target**: \<2 seconds Average time for your endpoint to respond **Target**: \<1% Percentage of deliveries that ultimately fail The delivery history shows: * **Successful deliveries** - Events delivered with 200 response * **Failed deliveries** - Events that couldn't be delivered * **Pending retries** - Events waiting to be retried * **Response details** - HTTP status codes and response times ## What's next? Now that you can test your webhooks effectively: Learn how to track webhook performance in production Advanced troubleshooting techniques for production issues Review delivery mechanics and performance optimization Ensure your webhook implementation is secure **Testing tip**: Test early, test often, and test everything. A well-tested webhook implementation prevents production surprises and ensures a reliable integration experience. # Delivery and Retries Source: https://docs.riseworks.io/webhooks/operations/delivery Understanding webhook delivery mechanics and retry policies Rise's webhook delivery system is designed for reliability and resilience. This guide explains how webhooks are delivered, what happens when deliveries fail, and how to optimize your endpoint for the best performance. ## How webhook delivery works When an event occurs in your Rise account, our system immediately begins the delivery process: A payment, deposit, or other action occurs in your Rise account Event is added to the delivery queue for each subscribed webhook endpoint Webhook payload is delivered to your endpoint via HTTP POST request Your endpoint's response determines whether delivery was successful Successful deliveries are logged; failures trigger the retry mechanism ## Delivery expectations ### Response requirements Your webhook endpoint should: Return a response within 10 seconds to avoid timeout Any status code from 200-299 indicates successful delivery Send a simple response like `{"received": true}` Process the same event multiple times safely (idempotency) ### What Rise considers successful Rise considers a webhook delivery successful when: * Your endpoint returns HTTP status **200-299** * The response is received within **10 seconds** * The connection completes without network errors ### What triggers a retry Rise will retry webhook deliveries when: * Your endpoint returns HTTP status **400-599** * The request times out after **10 seconds** * Network connection fails or is refused * DNS resolution fails for your endpoint ## Retry policy Rise implements an exponential backoff retry strategy to handle temporary failures gracefully. ### Retry schedule When a delivery fails, Rise will retry according to this schedule: **When**: After the initial failure **Wait time**: 1 minute (60 seconds) **Purpose**: Allow brief recovery time for temporary issues **When**: After first retry fails **Wait time**: 2 minutes (120 seconds) **Purpose**: Handle longer recovery periods **When**: After second retry fails **Wait time**: 5 minutes (300 seconds) **Purpose**: Maximum retry attempt with extended delay **Total retry period**: Up to approximately 8 minutes and 20 seconds from the original event **Maximum attempts**: 4 total (initial + 3 retries) **Retry timing**: Rise waits exactly **10 seconds** before initiating each retry attempt. This consistent delay applies to all retry attempts and helps ensure your endpoint has adequate time to recover from temporary issues. ### Exponential backoff benefits Gives your endpoint time to recover from issues Network glitches, brief downtime, or deployments Avoids overwhelming already struggling endpoints Multiple attempts significantly increase reliability ## Delivery status tracking Every webhook delivery is tracked and recorded for monitoring and debugging. ### Delivery states **Status**: Queued for delivery The webhook event has been created and is waiting in the delivery queue. This is the initial state before any delivery attempts. **Status**: Currently being delivered Rise is actively attempting to deliver the webhook to your endpoint. This state is usually very brief. **Status**: Successfully received Your endpoint responded with a 2xx status code within the timeout period. The delivery is complete and successful. **Status**: All retries exhausted All retry attempts have been exhausted and the webhook could not be delivered. Manual intervention may be required. **Status**: Scheduled for retry A delivery attempt failed but the webhook is scheduled to be retried according to the retry policy. ### Viewing delivery history Access delivery history through your webhook details page in the Rise app: Go to the Developer section and click on Webhooks Click on the webhook you want to monitor Click the "Delivery History" tab to see all delivery attempts Use filters to focus on successful, failed, or pending deliveries ## Optimizing your endpoint Follow these practices to ensure reliable webhook delivery: ### Response time optimization ```javascript theme={null} // Quick acknowledgment, async processing app.post('/webhooks', async (req, res) => { try { // Verify signature first with SDK const event = validator.validateEvent(req.body, req.headers['x-rise-signature']); // Acknowledge receipt immediately res.status(200).json({ received: true }); // Process event asynchronously processWebhookAsync(event); } catch (error) { res.status(400).send('Verification failed'); } }); async function processWebhookAsync(event) { // Queue for background processing await jobQueue.add('process-webhook', event); } ``` ```javascript theme={null} // Slow processing blocks response app.post('/webhooks', async (req, res) => { try { const event = validator.validateEvent(req.body, req.headers['x-rise-signature']); // DON'T: Slow operations before responding await updateDatabase(event); // 2-3 seconds await sendEmailNotification(event); // 1-2 seconds await callExternalAPI(event); // 3-5 seconds res.status(200).json({ received: true }); // Too late! } catch (error) { res.status(400).send('Error'); } }); ``` ### Idempotency handling Design your webhook handlers to safely process duplicate events: ```javascript theme={null} function handlePaymentEvent(event) { const paymentId = event.payment.nanoid; // Check if already processed const existingRecord = database.findPayment(paymentId); if (existingRecord && existingRecord.webhookProcessed) { console.log(`Payment ${paymentId} already processed, skipping`); return; // Safe to ignore duplicate } // Process the payment database.updatePayment(paymentId, { status: event.payment.status, webhookProcessed: true, processedAt: new Date() }); // Trigger business logic if (event.event_type === 'payment.sent') { fulfillOrder(event.payment.invoice_nanoid); } } ``` ### Error handling best practices ```javascript Recommended approach theme={null} app.post('/webhooks', async (req, res) => { try { // Always verify first with SDK const event = validator.validateEvent(req.body, req.headers['x-rise-signature']); // Respond quickly res.status(200).json({ received: true }); // Handle processing errors gracefully try { await processWebhook(event); } catch (processingError) { // Log error but don't fail the webhook console.error('Webhook processing error:', processingError); // Queue for retry or manual review await errorQueue.add('failed-webhook', { event: req.body, error: processingError.message, timestamp: new Date() }); } } catch (verificationError) { // Only fail webhook for verification errors console.error('Webhook verification failed:', verificationError); res.status(400).send('Invalid signature'); } }); ``` ```javascript What to avoid theme={null} app.post('/webhooks', async (req, res) => { try { const event = validator.validateEvent(req.body, req.headers['x-rise-signature']); // DON'T: Let processing errors fail the webhook await updateDatabase(event); // If this fails... await sendEmail(event); // ...webhook delivery fails await callAPI(event); // ...and gets retried unnecessarily res.status(200).json({ received: true }); } catch (error) { // This will cause unnecessary retries res.status(500).send('Processing failed'); } }); ``` ## Monitoring delivery performance Track your webhook delivery metrics to maintain reliability: ### Key metrics to monitor **Target**: >99% first-attempt success rate Percentage of deliveries that succeed without requiring retries **Target**: \<2 seconds average How quickly your endpoint responds to webhook requests **Target**: \<5% of deliveries Percentage of deliveries requiring retry attempts **Target**: \<0.1% of deliveries Percentage of deliveries that ultimately fail after all retries ## Delivery limits and quotas Rise implements reasonable limits to ensure system stability: ### Rate limits **Limit**: Reasonable per-endpoint rate Maximum webhook deliveries per second per endpoint **Limit**: Short burst capacity Temporary higher rate for event spikes **Limit**: Recovery time Brief pause between burst periods ### Retry and storage limits * **Maximum retries**: 3 attempts per webhook delivery * **Retry window**: Approximately 8 minutes total retry period * **Delivery history**: 30 days of delivery records stored * **Event retention**: 7 days of event data available for redelivery ## What's next? Now that you understand webhook delivery mechanics: Learn how to test your delivery optimization and error handling Deep dive into delivery tracking and performance analysis Advanced troubleshooting techniques for delivery issues Review security considerations for reliable delivery **Performance tip**: The fastest webhook is one that responds immediately and processes asynchronously. Your users will thank you for the responsiveness, and Rise will thank you for the reliability. # Monitoring Webhooks Source: https://docs.riseworks.io/webhooks/operations/monitoring Track webhook performance and health in production Effective webhook monitoring is crucial for maintaining reliable integrations. This guide covers how to monitor webhook performance, and diagnose issues before they impact your users. ## Monitoring overview Rise provides comprehensive webhook monitoring through the dashboard: Built-in analytics showing delivery success rates and response times Monitor individual webhook deliveries and their status Track response times and identify performance trends Identify patterns in webhook failures and errors ## Rise dashboard monitoring The Rise app provides comprehensive webhook monitoring out of the box: ### Accessing webhook analytics Go to **Settings** → **Webhooks** in your Rise dashboard Click on the webhook you want to monitor Click the **"Analytics"** tab to see performance metrics Switch to **"Delivery History"** for detailed event logs ### Key metrics in Rise dashboard **Last 24h**: 99.2% Percentage of successful first-attempt deliveries **Last 24h**: 1.2s Average time for your endpoint to respond **Last 24h**: 1,247 Number of webhook events delivered ### Delivery status tracking The delivery history shows detailed information for each webhook: * **Delivered** - Successful delivery with 2xx response * **Retrying** - Failed delivery, retries in progress * **Failed** - All retry attempts exhausted * **Response details** - HTTP status, response time, headers ## Monitoring checklist Ensure comprehensive webhook monitoring coverage: * Regular review of webhook analytics * Delivery history monitoring * Response time tracking * Error pattern analysis * Success rate trending * Event volume monitoring ## Rise dashboard features The Rise dashboard provides all the monitoring capabilities you need for webhook delivery: **Detailed logs**: View every webhook delivery attempt with full details **Filter options**: Search by status, date, event type, or response code **Error analysis**: Identify common failure patterns and root causes **Response details**: View full HTTP responses and error messages ## What's next? With monitoring in place, you're prepared to handle issues proactively: Learn how to diagnose and fix common webhook issues Review testing strategies to prevent issues Ensure your monitoring doesn't expose sensitive data Technical reference for webhook event types and payloads **Monitoring tip**: Start with basic monitoring and gradually add more sophisticated metrics as your webhook usage grows. Focus on user impact metrics alongside technical metrics. # Troubleshooting Webhooks Source: https://docs.riseworks.io/webhooks/operations/troubleshooting Diagnose and resolve common webhook issues When webhooks aren't working as expected, systematic troubleshooting can help you quickly identify and resolve issues. This guide covers common problems, diagnostic techniques, and solutions for webhook integration issues. ## Quick diagnosis checklist Start with these basic checks to identify the most common issues: **Check these first:** * Webhook URL is correct and accessible * Event types are properly subscribed * Webhook is active (not paused) * No firewall blocking Rise servers * SSL certificate is valid (for HTTPS endpoints) **Common causes:** * Incorrect webhook secret * Wrong signature parsing logic * JSON stringification mismatch * Character encoding problems **Performance issues:** * Database queries taking too long * External API calls blocking * Heavy processing in webhook handler * Memory leaks causing slowdown * Insufficient server resources **Reliability problems:** * Network connectivity issues * Load balancer misconfigurations * Server restarts during processing * Database connection pool exhaustion * Race conditions in processing logic ## Common error scenarios ### HTTP status code errors Different HTTP responses indicate different types of issues: **Common causes:** * Invalid signature verification * Malformed JSON in response * Missing required headers * Request body parsing errors **Resolution:** Check signature logic and request handling **Common causes:** * Missing authentication headers * Invalid API keys or tokens * Expired authentication credentials **Resolution:** Verify authentication configuration **Common causes:** * Incorrect webhook URL path * Server routing misconfiguration * Application not running **Resolution:** Verify URL and server configuration **Common causes:** * Unhandled exceptions in webhook code * Database connection failures * External service timeouts **Resolution:** Check application logs and error handling **Common causes:** * Load balancer cannot reach backend * Application server crashed * Proxy configuration issues **Resolution:** Check infrastructure and application health **Common causes:** * Server overloaded or maintenance * Rate limiting triggered * Circuit breaker activated **Resolution:** Check server capacity and rate limits ### Signature verification issues Signature verification is the most common source of webhook problems: **Step-by-step debugging:** ```javascript theme={null} function debugSignatureVerification(payload, signature, secret) { console.log('Debugging signature verification:'); console.log('Received signature:', signature); console.log('Webhook secret:', secret ? 'Present' : 'Missing'); // Parse signature components const elements = signature.split(','); console.log('Signature elements:', elements); if (elements.length !== 2) { console.error('Invalid signature format - should have timestamp and hash'); return false; } const timestamp = elements[0].split('=')[1]; const receivedHash = elements[1].split('=')[1]; console.log('Extracted timestamp:', timestamp); console.log('Extracted hash:', receivedHash); // Create signed payload const signedPayload = `${timestamp}.${JSON.stringify(payload)}`; console.log('Signed payload:', signedPayload); // Generate expected hash const expectedHash = crypto .createHmac('sha256', secret) .update(signedPayload) .digest('hex'); console.log('Expected hash:', expectedHash); console.log('Received hash:', receivedHash); console.log('Hashes match:', expectedHash === receivedHash); return expectedHash === receivedHash; } ``` **Mistake 1: Wrong JSON stringification** ```javascript theme={null} // Wrong - includes spaces const signedPayload = `${timestamp}.${JSON.stringify(payload, null, 2)}`; // Correct - compact format const signedPayload = `${timestamp}.${JSON.stringify(payload)}`; ``` **Mistake 2: Incorrect secret** ```javascript theme={null} // Wrong - using wrong secret const secret = 'webhook-secret-key'; // Generic secret // Correct - use exact secret from Rise webhook settings const secret = process.env.RISE_WEBHOOK_SECRET; // From Rise dashboard ``` **Mistake 3: Wrong header name** ```javascript theme={null} // Wrong - incorrect header case const signature = req.headers['X-Rise-Signature']; // Correct - lowercase header name const signature = req.headers['x-rise-signature']; ``` **Create test signatures for debugging:** ```javascript theme={null} function generateTestSignature(payload, secret) { const timestamp = Math.floor(Date.now() / 1000); const signedPayload = `${timestamp}.${JSON.stringify(payload)}`; const hash = crypto .createHmac('sha256', secret) .update(signedPayload) .digest('hex'); return `t=${timestamp},v1=${hash}`; } // Test with known payload const testPayload = { id: 'we-test123', object: 'event', created: Math.floor(Date.now() / 1000), event_type: 'payment.sent', event_version: '1.0', payment: { nanoid: 'pa-test123' } }; const testSignature = generateTestSignature(testPayload, 'your-secret'); console.log('Test signature:', testSignature); // Verify it works with SDK import { WebhookValidator } from '@riseworks/sdk'; const validator = new WebhookValidator({ secret: 'your-secret', tolerance: 600 }); try { const event = validator.validateEvent(testPayload, testSignature); console.log('Verification result: Valid'); } catch (error) { console.log('Verification result: Invalid -', error.message); } ``` ## What's next? After resolving webhook issues: Set up comprehensive monitoring to prevent future issues Implement thorough testing to catch issues early Learn about webhook version migrations and updates Technical reference for webhook event types and payloads **Troubleshooting tip**: Keep detailed logs during debugging, but remember to remove sensitive data before sharing logs with support or team members. # Migration Guide Source: https://docs.riseworks.io/webhooks/reference/migration Upgrading from Rise Webhooks v1 to v2 **Important**: Rise Webhooks v1 is deprecated and will be discontinued. All customers must migrate to v2 by **\[DATE]**. After this date, v1 webhooks will no longer be supported. Rise Webhooks v2 introduces new features and enhancements in reliability, security, and event structure. This guide helps you migrate from v1 to the new v2 architecture. **SDK Required**: For v2 webhook handling, you'll need to install the Rise SDK: `npm install @riseworks/sdk` (or `yarn add @riseworks/sdk` / `pnpm add @riseworks/sdk`) ## Understanding the changes The migration from v1 to v2 involves fundamental changes to webhook structure, security, and event types. v2 uses a consistent envelope format for all events with enhanced metadata Replaced numeric IDs with human-readable nanoids for better tracking Each event includes version information for future compatibility HMAC-based signature verification with individual customer secrets ### Webhook payload structure Will be discontinued: This structure will no longer be available after v1 deprecation. ```json theme={null} { "type": "deposit.deposit_received", "idempotent_key": "0x012321", "company_id": 1, "timestamp": 1700242816, "transaction": { // Event-specific data } } ``` ```json theme={null} { "object": "event", "created": 1751590453, "request_id": "req-1751590452487", "event_type": "payment.sent", "event_version": "2.0", "idempotency_key": "85420805-0b5e-4b11-b7f4-c6f05db7120b", // Event-specific data structure } ``` ## Security model changes ### v1 security (deprecated) Deprecated: v1 security model will be discontinued with v1 deprecation. v1 used a public key verification system: ```javascript v1 verification (DEPRECATED) theme={null} const hash = req.headers['x-rise-hash']; const signature = req.headers['x-rise-signature']; // Verify hash const bodyHash = ethers.id(JSON.stringify(body)); if (bodyHash !== hash) { return res.status(400).send('Invalid hash'); } // Verify signature with the public key from your dashboard const riseSigner = '0x1234'; // Public key from your Rise dashboard const recoveredAddress = ethers.verifyMessage(hash, signature); if (recoveredAddress !== riseSigner) { return res.status(400).send('Invalid signature'); } ``` ### v2 security (current) v2 uses HMAC-based signature verification with your own secret. The Rise SDK provides built-in webhook validation: ```javascript v2 verification with SDK (CURRENT) theme={null} const { WebhookValidator } = require('@riseworks/sdk'); // Initialize webhook validator with your secret const validator = new WebhookValidator({ secret: process.env.RISE_WEBHOOK_SECRET, tolerance: 300 // 5 minutes tolerance for timestamp validation }); // Verify signature and get validated event const event = validator.validateEvent(req.body, req.headers['x-rise-signature']); console.log('Validated event:', event); ``` **Alternative: Safe validation with error handling** ```javascript v2 safe verification with SDK theme={null} const { WebhookValidator } = require('@riseworks/sdk'); const validator = new WebhookValidator({ secret: process.env.RISE_WEBHOOK_SECRET, tolerance: 300 }); try { // Safe validation that returns null on failure instead of throwing const event = validator.validateEventSafe(req.body, req.headers['x-rise-signature']); if (event) { console.log('Validated event:', event); // Process the event } else { console.error('Invalid webhook signature'); res.status(400).send('Invalid signature'); return; } } catch (error) { console.error('Webhook validation error:', error.message); res.status(400).send('Validation failed'); return; } ``` **Alternative: Safe validation with error handling** ```javascript v2 safe verification with SDK theme={null} const { WebhookValidator } = require('@riseworks/sdk'); const validator = new WebhookValidator({ secret: process.env.RISE_WEBHOOK_SECRET, tolerance: 300 }); try { // Safe validation that returns null on failure instead of throwing const event = validator.validateEventSafe(req.body, req.headers['x-rise-signature']); if (event) { console.log('Validated event:', event); // Process the event } else { console.error('Invalid webhook signature'); res.status(400).send('Invalid signature'); return; } } catch (error) { console.error('Webhook validation error:', error.message); res.status(400).send('Validation failed'); return; } ``` Each customer manages their own webhook secret Timestamps prevent old events from being replayed Industry-standard signature method ## Event type mapping Important: All v1 event types listed below will be discontinued when v1 is deprecated. Ensure you update your integration to handle v2 event types. | v1 Event Type | v2 Event Type | Changes | | ------------------------------------ | -------------------------------------- | ----------------------------------- | | `deposit.deposit_received` | `deposit.received` | Simplified naming, enhanced payload | | `payment.payment_sent` | `payment.sent` | Cleaner naming, structured data | | `pay_schedules.pay_schedule_created` | `payment.group.created` | Renamed to reflect payment groups | | `invites.invite_accepted` | `invite.accepted` | Simplified naming | | `account_duplicated.detected` | `withdraw_account.duplicated_detected` | More specific naming | | `payee.riseid_address_updated` | *(Deprecated)* | No longer supported in v2 | ## Recommended migration strategy **Migration Required**: Since v1 will be deprecated, migration to v2 is mandatory, not optional. We recommend the **gradual migration approach** to minimize risk and ensure a smooth transition: Support both v1 and v2 webhooks during the transition period: ```javascript theme={null} // Support both v1 and v2 webhooks during transition app.post('/webhooks/v1', handleV1Webhook); app.post('/webhooks/v2', handleV2Webhook); function handleV1Webhook(req, res) { // Existing v1 logic with v1 signature verification const hash = req.headers['x-rise-hash']; const signature = req.headers['x-rise-signature']; // v1 verification (keep existing logic) verifyV1Signature(req.body, hash, signature); // Convert to v2 format for unified processing const v2Event = convertV1ToV2(req.body); processWebhookEvent(v2Event); res.status(200).send('OK'); } function handleV2Webhook(req, res) { // New v2 logic with SDK const { WebhookValidator } = require('@riseworks/sdk'); const validator = new WebhookValidator({ secret: process.env.RISE_WEBHOOK_SECRET, tolerance: 300 }); try { // Validate webhook signature using SDK const event = validator.validateEvent(req.body, req.headers['x-rise-signature']); processWebhookEvent(event); res.status(200).json({ received: true }); } catch (error) { console.error('Webhook validation failed:', error.message); res.status(400).send('Invalid signature'); } } // Convert v1 events to v2 format for unified processing function convertV1ToV2(v1Event) { // Your conversion logic here based on event type return { object: 'event', created: v1Event.timestamp, request_id: `v1-${v1Event.idempotent_key}`, event_type: mapV1ToV2EventType(v1Event.type), event_version: '1.0', idempotency_key: v1Event.idempotent_key, // Map v1 data to v2 structure ...v1Event.transaction }; } function mapV1ToV2EventType(v1Type) { const mapping = { 'deposit.deposit_received': 'deposit.received', 'payment.payment_sent': 'payment.sent', 'pay_schedules.pay_schedule_created': 'payment.group.created', 'invites.invite_accepted': 'invite.accepted', 'account_duplicated.detected': 'withdraw_account.duplicated_detected' }; return mapping[v1Type] || v1Type; } ``` Create a single event processor that handles both formats: ```javascript theme={null} function processWebhookEvent(event) { // Your unified event processing logic here // Handle both v1 and v2 event formats } ``` 1. **Navigate to webhook settings** in the Rise app 2. **Create new webhook** with your `/webhooks/v2` endpoint 3. **Generate webhook secret** and store it securely 4. **Subscribe to same events** as your v1 webhook 5. **Test the v2 webhook** using Rise's test feature Run both webhooks in parallel while tracking migration progress: ```javascript theme={null} // Add monitoring to track migration progress const migrationStats = { v1Count: 0, v2Count: 0, v1Errors: 0, v2Errors: 0 }; function handleV1Webhook(req, res) { migrationStats.v1Count++; try { // Your v1 processing logic here const v2Event = convertV1ToV2(req.body); processWebhookEvent(v2Event); res.status(200).send('OK'); } catch (error) { migrationStats.v1Errors++; console.error('V1 webhook error:', error.message); res.status(400).send('Error'); } } function handleV2Webhook(req, res) { migrationStats.v2Count++; try { // V2 processing with SDK const { WebhookValidator } = require('@riseworks/sdk'); const validator = new WebhookValidator({ secret: process.env.RISE_WEBHOOK_SECRET, tolerance: 300 }); const event = validator.validateEvent(req.body, req.headers['x-rise-signature']); processWebhookEvent(event); res.status(200).json({ received: true }); } catch (error) { migrationStats.v2Errors++; console.error('V2 webhook error:', error.message); res.status(400).send('Invalid signature'); } } // Log stats periodically setInterval(() => { console.log('Migration stats:', migrationStats); }, 60000); // Every minute ``` Once you're confident v2 is working correctly: 1. **Deactivate v1 webhook** in the Rise app 2. **Monitor v2 webhook** for any issues 3. **Remove v1 endpoint** from your code after a few days 4. **Clean up migration code** once fully migrated ## Migration checklist **Time-Sensitive**: Complete migration before v1 deprecation date to avoid service disruption. * Review v1 webhook usage in your application * Identify all event types currently processed * Plan migration strategy * Set up testing environment * Generate webhook secret for v2 * Implement v2 webhook endpoint * Add signature verification for v2 * Update event processing logic * Test both v1 and v2 events during parallel operation * Monitor webhook delivery success rates * Remove v1 webhook endpoints when no longer needed * Update documentation and team knowledge * Monitor v2 webhook functionality * Remove v1 compatibility code after transition period * Update monitoring and alerting for new event types ## Getting help with migration If you encounter issues during migration: Common migration problems and solutions Use your Rise dashboard to check webhook delivery status Migrate one event type at a time for safer deployment Our support team can assist with complex migrations ## What's next? After completing your migration to v2: Track v2 delivery and webhook health Take advantage of v2 features and improvements Use enhanced event data and new event types Complete v2 event types and payload documentation **Migration tip**: Take your time with migration, but don't delay too long. It's better to migrate carefully over a few days than to rush and break production systems. The gradual migration approach allows you to validate each step before proceeding. **Important reminder**: v1 webhooks will be discontinued on **\[DATE]**. Ensure your migration is complete before this date to avoid service interruption.