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

# 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.

<Note>
  All payment endpoints here require `Authorization: Bearer <jwt>` — see [Authentication](/v1-legacy/authentication).
</Note>

<Note>
  `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).
</Note>

<Warning>
  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.
</Warning>

## Prepare an instant payment

**PUT** `/v1/payments/pay`

<ParamField body="wallet" type="string" required>Wallet address of the payer.</ParamField>
<ParamField body="rise_id" type="string" required>V1/V2 RiseID, RiseAccount address, or nanoid of the paying team.</ParamField>
<ParamField body="payee" type="string" required>V1/V2 RiseID, RiseAccount address, or nanoid of the payee.</ParamField>
<ParamField body="amount" type="number | string" required>Payment amount in micro-USD (`1,000,000` = `$1.00`) — e.g. `100000000` for \$100.00.</ParamField>

<ParamField body="salt" type="number | string" required>
  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.
</ParamField>

<RequestExample>
  ```bash theme={null}
  curl -X PUT "https://b2b-api.riseworks.io/v1/payments/pay" \
    -H "Authorization: Bearer <jwt>" \
    -H "Content-Type: application/json" \
    -d '{
      "wallet": "0xYourWalletAddress",
      "rise_id": "te-abc123def456",
      "payee": "us-xyz789abc123",
      "amount": 100000000,
      "salt": 1
    }'
  ```
</RequestExample>

<ResponseExample>
  ```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": {}
    }
  }
  ```
</ResponseExample>

`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.

<Note>
  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.
</Note>

## Execute an instant payment

**POST** `/v1/payments/pay`

Same body as the prepare step, plus the signed request and signature:

<ParamField body="request" type="object" required>The typed-data request object returned by the prepare step.</ParamField>
<ParamField body="signature" type="string" required>Signature over the typed data.</ParamField>
<ParamField body="title" type="string">Optional payment title (max 60 chars).</ParamField>
<ParamField body="description" type="string">Optional payment description (max 1000 chars).</ParamField>
<ParamField body="external_id" type="string">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.)</ParamField>

<ResponseExample>
  ```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
    }
  }
  ```
</ResponseExample>

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,
});
```

<Note>
  `client.v1Legacy.batchPay()` / `prepareBatchPay()` / `executeBatchPay()` follow the identical pattern for `/v1/payments/batch-pay`.
</Note>

### 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);
```

<Note>
  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.
</Note>

## Prepare a batch payment

**PUT** `/v1/payments/batch-pay`

<ParamField body="wallet" type="string" required>Wallet address of the payer.</ParamField>
<ParamField body="rise_id" type="string" required>V1/V2 RiseID, RiseAccount address, or nanoid of the paying team.</ParamField>
<ParamField body="total_amount" type="number | string" required>Micro-USD. Must equal the sum of all `payments[].amount`.</ParamField>

<ParamField body="payments" type="array" required>
  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`.
</ParamField>

<RequestExample>
  ```bash theme={null}
  curl -X PUT "https://b2b-api.riseworks.io/v1/payments/batch-pay" \
    -H "Authorization: Bearer <jwt>" \
    -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 }
      ]
    }'
  ```
</RequestExample>

<ResponseExample>
  ```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"
    }
  }
  ```
</ResponseExample>

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.

<ParamField body="company_id" type="number | string" required>Legacy numeric id, V1/V2 RiseID, RiseAccount address, or nanoid of the paying company/team.</ParamField>

<ParamField body="pay_intents" type="array" required>
  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.
</ParamField>

<RequestExample>
  ```bash theme={null}
  curl -X POST "https://b2b-api.riseworks.io/v1/payments/batch-pay/intents" \
    -H "Authorization: Bearer <jwt>" \
    -H "Content-Type: application/json" \
    -d '{
      "company_id": "te-abc123def456",
      "pay_intents": [
        {
          "payee_riseid": "us-xyz789abc123",
          "amount": 100000000,
          "salt": 1,
          "timestamp": 1735689600
        }
      ]
    }'
  ```
</RequestExample>

<ResponseExample>
  ```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"
      }
    ]
  }
  ```
</ResponseExample>

<Warning>
  Returns `409 Conflict` if a payment group or payment with the same salt already exists — use a new salt to retry.
</Warning>

**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`

<ParamField query="company_id" type="number | string" required>Legacy numeric id, V1/V2 RiseID, RiseAccount address, or nanoid of the paying company/team.</ParamField>
<ParamField query="external_ids" type="string | string[]">Filter by one or more caller-supplied external ids.</ParamField>
<ParamField query="payee_ids" type="string | string[]">Filter by one or more payees — V1/V2 RiseID, RiseAccount address, or nanoid.</ParamField>
<ParamField query="page" type="number">1-indexed page number (default 1).</ParamField>
<ParamField query="count" type="number">Page size, 1–1000 (default 100). Values above 1000 are silently capped, not rejected.</ParamField>

<ParamField query="status" type="string">
  `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`).
</ParamField>

<Warning>
  `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.
</Warning>

<RequestExample>
  ```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 <jwt>"
  ```
</RequestExample>

<ResponseExample>
  ```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..."
        }
      }
    ]
  }
  ```
</ResponseExample>

**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).
