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

# Create Stream

> Create token-based or point-based incentive streams for your organization

<Note>
  All requests require an API key via the `X-API-Key` header.
  See [Authentication](/sdk/authentication/api-keys) for details.
</Note>

<Warning>
  To create streams, the organization attached to the API key must also have permission to create streams. If your organization still does not have that permission enabled, contact the Turtle team to request it.
</Warning>

## Overview

`POST /v1/streams/` creates a new incentive stream owned by the organization attached to the API key.

There are two creation flows:

* **Token-based stream:** the API validates the request, stores the pending stream, and returns `txParams` with the backend-signed authorization that your wallet must submit on-chain to the corresponding `StreamFactory`.
* **Point-based stream:** the API creates the stream immediately and returns `txParams: null`.

Points are only used for point-based streams. If your stream uses `rewardTokenId`, you do not need to create or reference a point.

<Info>
  A stream can use both `rewardTokenId` and `targetTokenId` on the same chain. The difference is support scope, not a requirement to split them across networks. Reward-token selection is currently supported only for stream creation on 5 networks: Ethereum, Base, Avalanche, BSC, and Sepolia. `targetTokenId` supports a broader set of chains through [Get Tokens](/sdk/streams/get-tokens), where the lookup uses the decimal EVM `chainId`.
</Info>

## Supported Stream Types

| Type | Strategy        | Required behavior                              |
| ---- | --------------- | ---------------------------------------------- |
| `1`  | `Fixed Rate`    | `totalAmount` required for token-based streams |
| `2`  | `Fixed APR`     | `rewardTokenId` and `totalAmount` required     |
| `3`  | `Variable Rate` | `totalAmount` must be omitted                  |

## Create a Token-Based Stream

<Info>
  If you are creating a token-based stream, use [Get Tokens](/sdk/streams/get-tokens) to list the supported reward-token UUIDs (`rewardTokenId`) for the selected chain, or to discover supported target tokens used in strategy `customArgs` (for example, `targetTokenId`). For `rewardTokenId`, choose a token where `isAllowedRewardToken` is `true`.
</Info>

<Warning>
  For token-based streams, the `totalAmount` specified in the request is the exact amount used in the on-chain stream-creation transaction. This amount must already be available in the submitting wallet, as it will be transferred to the smart contract when the stream creation is finalized on-chain.
</Warning>

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST "https://earn.turtle.xyz/v1/streams/" \
    -H "X-API-Key: sk_live_xxxxx" \
    -H "Content-Type: application/json" \
    -d '{
      "walletAddress": "0x1111111111111111111111111111111111111111",
      "type": 2,
      "rewardTokenId": "56b0fab0-5c3e-49f6-a0a7-57e38d5ea999",
      "totalAmount": "2500000000000000000000",
      "startTimestamp": "2026-03-20T00:00:00Z",
      "endTimestamp": "2026-04-20T00:00:00Z",
      "customArgs": {
        "targetTokenId": "8cc2ed9d-bd59-42fd-9df5-329fa22497b6",
        "apr": "0.12"
      },
      "adapters": []
    }'
  ```

  ```typescript TypeScript theme={null}
  const response = await fetch('https://earn.turtle.xyz/v1/streams/', {
    method: 'POST',
    headers: {
      'X-API-Key': process.env.TURTLE_SECRET_KEY!,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      walletAddress: '0x1111111111111111111111111111111111111111',
      type: 2,
      rewardTokenId: '56b0fab0-5c3e-49f6-a0a7-57e38d5ea999',
      totalAmount: '2500000000000000000000',
      startTimestamp: '2026-03-20T00:00:00Z',
      endTimestamp: '2026-04-20T00:00:00Z',
      customArgs: {
        targetTokenId: '8cc2ed9d-bd59-42fd-9df5-329fa22497b6',
        apr: '0.12',
      },
      adapters: [],
    }),
  });

  const data = await response.json();
  ```
</CodeGroup>

## Create a Point-Based Stream

<Info>
  If you are creating a point-based stream and do not yet have a point asset for your organization, use [Create Point](/sdk/streams/create-point) first. You can list existing organization points with [Get Points](/sdk/streams/get-points).
</Info>

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST "https://earn.turtle.xyz/v1/streams/" \
    -H "X-API-Key: sk_live_xxxxx" \
    -H "Content-Type: application/json" \
    -d '{
      "type": 1,
      "pointId": "9a598b70-c6f9-4a1f-9357-3a5823a7ce36",
      "totalAmount": "5000000000000000000000",
      "startTimestamp": "2026-03-20T00:00:00Z",
      "endTimestamp": "2026-04-20T00:00:00Z",
      "customArgs": {
        "targetTokenId": "8cc2ed9d-bd59-42fd-9df5-329fa22497b6",
        "tokensPerUSD": "1000000000000000"
      },
      "adapters": []
    }'
  ```

  ```typescript TypeScript theme={null}
  const response = await fetch('https://earn.turtle.xyz/v1/streams/', {
    method: 'POST',
    headers: {
      'X-API-Key': process.env.TURTLE_SECRET_KEY!,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      type: 1,
      pointId: '9a598b70-c6f9-4a1f-9357-3a5823a7ce36',
      totalAmount: '5000000000000000000000',
      startTimestamp: '2026-03-20T00:00:00Z',
      endTimestamp: '2026-04-20T00:00:00Z',
      customArgs: {
        targetTokenId: '8cc2ed9d-bd59-42fd-9df5-329fa22497b6',
        tokensPerUSD: '1000000000000000',
      },
      adapters: [],
    }),
  });

  const data = await response.json();
  ```
</CodeGroup>

## Request Body

<ParamField body="walletAddress" type="string">
  Admin EVM address for token-based streams. Must be omitted for point-based streams.
</ParamField>

<ParamField body="type" type="integer" required>
  Stream type. Supported values are `1`, `2`, and `3`.
</ParamField>

<ParamField body="rewardTokenId" type="uuid">
  Reward token UUID (use the token `id` from [Get Tokens](/sdk/streams/get-tokens)) for token-based streams. Exactly one of `rewardTokenId` or `pointId` must be provided. Choose a token where `isAllowedRewardToken` is `true`. Reward-token selection for stream creation is currently limited to Ethereum, Base, Avalanche, BSC, and Sepolia; `rewardTokenId` will be resolved by the server into the token address and chain used for on-chain creation.
</ParamField>

<ParamField body="pointId" type="uuid">
  Point identifier for point-based streams only. Exactly one of `rewardTokenId` or `pointId` must be provided. See [Get Points](/sdk/streams/get-points) for the `Point` schema and examples.
</ParamField>

<ParamField body="totalAmount" type="string">
  Total rewards in base units. Required for token-based fixed-rate and fixed-APR streams. Must be omitted for variable-rate streams. For token-based streams, this is the amount that will be sent to the smart contract onchain when the wallet submits the returned `txParams`, so the wallet must already hold the required balance.
</ParamField>

<ParamField body="startTimestamp" type="datetime" required>
  UTC stream start timestamp. Must align to a 15-minute boundary.
</ParamField>

<ParamField body="endTimestamp" type="datetime">
  Optional UTC stream end timestamp. If provided, it must be at least one hour after `startTimestamp` and aligned to a 15-minute boundary. For token-based variable-rate streams, it is required.
</ParamField>

<ParamField body="customArgs" type="object" required>
  Strategy-specific configuration object. See [customArgs by Type](#customargs-by-type) for details.
</ParamField>

<ParamField body="adapters" type="array" default="[]">
  Optional adapter configuration array. Each adapter entry must include `type` and `params`.
</ParamField>

## `customArgs` by Type

### Fixed Rate (`type = 1`)

```json theme={null}
{
  "targetTokenId": "8cc2ed9d-bd59-42fd-9df5-329fa22497b6",
  "tokensPerUSD": "1000000000000000"
}
```

* `targetTokenId`: supported target token used as the tracked balance source. Resolve it through [Get Tokens](/sdk/streams/get-tokens) using the decimal EVM `chainId` for the target-token network.
* `tokensPerUSD`: positive reward-emission coefficient in base units. Specifies the daily number of token/point wei units to grant for every 1,000 USD of provisioned TVL.

### Fixed APR (`type = 2`)

```json theme={null}
{
  "targetTokenId": "8cc2ed9d-bd59-42fd-9df5-329fa22497b6",
  "apr": "0.12"
}
```

* `targetTokenId`: supported target token used as the tracked balance source. Resolve it through [Get Tokens](/sdk/streams/get-tokens) using the decimal EVM `chainId` for the target-token network.
* `apr`: positive decimal APR value.

### Variable Rate (`type = 3`)

```json theme={null}
{
  "targetTokenId": "8cc2ed9d-bd59-42fd-9df5-329fa22497b6",
  "tokensPerDay": "1000000000000000000"
}
```

* `targetTokenId`: supported target token used as the tracked balance source. Resolve it through [Get Tokens](/sdk/streams/get-tokens) using the decimal EVM `chainId` for the target-token network.
* `tokensPerDay`: positive per-day reward budget in base units.

## Response Examples

### Token-based response

```json theme={null}
{
  "message": "Successfully generated signature for token-based stream",
  "txParams": {
    "chainId": 1,
    "sender": "0x1111111111111111111111111111111111111111",
    "params": {
      "params": {
        "StreamId": [85, 14, 132, 0, 226, 155, 65, 212, 167, 22, 68, 102, 85, 68, 0, 0],
        "RewardToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
        "NetTotalAmount": "2500000000000000000000",
        "FeeAmount": "0"
      },
      "deadline": "1773628800",
      "signature": "MEUCIA...base64..."
    }
  },
  "stream": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "chainId": 1,
    "contractAddress": null,
    "userId": null,
    "orgId": "9f51b66a-d13a-4b55-8515-ae6e4ef7cf25",
    "admin": "0x1111111111111111111111111111111111111111",
    "type": 2,
    "createdAt": "2026-03-20T00:00:00Z",
    "updatedAt": "2026-03-20T00:00:00Z",
    "startTimestamp": "2026-03-20T00:00:00Z",
    "endTimestamp": "2026-04-20T00:00:00Z",
    "totalAmount": "2500000000000000000000",
    "creationConfirmedAt": null,
    "snapshotComputationPaused": false,
    "merkleTreeComputationPaused": false,
    "hashCommitmentPaused": false,
    "claimPaused": false,
    "customArgs": {
      "targetTokenId": "8cc2ed9d-bd59-42fd-9df5-329fa22497b6",
      "apr": "0.12"
    },
    "adapters": [],
    "point": null,
    "strategy": "Fixed APR",
    "lastSnapshot": null,
    "committedSnapshot": null,
    "rewardToken": {
      "id": "56b0fab0-5c3e-49f6-a0a7-57e38d5ea999",
      "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
      "name": "USD Coin",
      "symbol": "USDC",
      "decimals": 6,
      "chainId": 1,
      "logoUrl": "https://cdn.example.com/tokens/usdc.png",
      "isAllowedRewardToken": true
    }
  }
}
```

### Point-based response

```json theme={null}
{
  "message": "Successfully created point-based stream",
  "txParams": null,
  "stream": {
    "id": "550e8400-e29b-41d4-a716-446655440001",
    "chainId": null,
    "contractAddress": null,
    "userId": null,
    "orgId": "9f51b66a-d13a-4b55-8515-ae6e4ef7cf25",
    "admin": null,
    "type": 1,
    "createdAt": "2026-03-20T00:00:00Z",
    "updatedAt": "2026-03-20T00:00:00Z",
    "startTimestamp": "2026-03-20T00:00:00Z",
    "endTimestamp": "2026-04-20T00:00:00Z",
    "totalAmount": "5000000000000000000000",
    "creationConfirmedAt": "2026-03-20T00:00:00Z",
    "snapshotComputationPaused": false,
    "merkleTreeComputationPaused": false,
    "hashCommitmentPaused": false,
    "claimPaused": false,
    "customArgs": {
      "targetTokenId": "8cc2ed9d-bd59-42fd-9df5-329fa22497b6",
      "tokensPerUSD": "1000000000000000"
    },
    "adapters": [],
    "point": {
      "id": "9a598b70-c6f9-4a1f-9357-3a5823a7ce36",
      "orgId": "9f51b66a-d13a-4b55-8515-ae6e4ef7cf25",
      "symbol": "POINT",
      "name": "Partner Points",
      "decimals": 18,
      "logoUrl": "https://cdn.example.com/points/partner-points.png",
      "createdAt": "2026-03-01T00:00:00Z",
      "updatedAt": "2026-03-01T00:00:00Z"
    },
    "strategy": "Fixed Rate",
    "lastSnapshot": null,
    "committedSnapshot": null,
    "rewardToken": null
  }
}
```

## Response Fields

<ResponseField name="message" type="string" required>
  Status message describing the creation path used.
</ResponseField>

<ResponseField name="txParams" type="object | null" required>
  On-chain creation payload for token-based streams. Submit it to the corresponding `StreamFactory` on `txParams.chainId`. `null` for point-based streams.
</ResponseField>

<ResponseField name="txParams.chainId" type="integer" required>
  Chain where the stream-factory transaction must be submitted.
</ResponseField>

<ResponseField name="txParams.sender" type="string" required>
  Wallet expected to submit the transaction.
</ResponseField>

<ResponseField name="txParams.params.params.StreamId" type="number[]" required>
  16-byte stream ID encoded as a byte array for the stream factory call.
</ResponseField>

<ResponseField name="txParams.params.params.RewardToken" type="string" required>
  Reward token contract address.
</ResponseField>

<ResponseField name="txParams.params.params.NetTotalAmount" type="string" required>
  Streamed amount in decimal-string form to preserve precision.
</ResponseField>

<ResponseField name="txParams.params.params.FeeAmount" type="string" required>
  Creation fee amount in decimal-string form.
</ResponseField>

<ResponseField name="txParams.params.deadline" type="string" required>
  Unix timestamp deadline for the signed payload.
</ResponseField>

<ResponseField name="txParams.params.signature" type="string" required>
  EIP-712 signature bytes serialized as base64 in JSON.
</ResponseField>

<ResponseField name="stream" type="Stream" required>
  Persisted stream record created by the request. It uses the same public stream schema returned by [Get Streams](/sdk/streams/get-streams).
</ResponseField>

<ResponseField name="stream.adapters" type="AdapterConfig[]" required>
  Persisted adapter configuration array. Each item contains a `type` string and a `params` object.
</ResponseField>

<ResponseField name="stream.rewardToken" type="SupportedToken | null" required>
  Reward-token metadata for token-based streams. This uses the same token shape returned by [Get Tokens](/sdk/streams/get-tokens).
</ResponseField>

## Broadcast the Token-Based Transaction

For token-based streams, the backend does not return a fully serialized raw transaction. Instead, it returns a signed authorization payload in `txParams` that your wallet must use to call `createStream` on the `StreamFactory` for the target chain.

The wallet that sends the transaction must:

* match `txParams.sender`
* be connected to `txParams.chainId`
* approve the `StreamFactory` to transfer the reward token amount needed for `NetTotalAmount + FeeAmount`

### `StreamFactory` addresses by chain

| `chainId`  | Network   | `StreamFactory`                              |
| ---------- | --------- | -------------------------------------------- |
| `1`        | Ethereum  | `0xf44399a74ee5ddef7fa3d064cf66b011ee4a6cae` |
| `56`       | BSC       | `0x298d2967588b5c93a137ce1a05d0b8cfffb3c120` |
| `43114`    | Avalanche | `0x4559605e3003fda8c059e14af4f16ba9a004335a` |
| `8453`     | Base      | `0x4559605e3003fda8c059e14af4f16ba9a004335a` |
| `11155111` | Sepolia   | `0xdfdff939d728585ce8a2cf2d4166f043d917d8d2` |

### TypeScript example

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

const STREAM_FACTORY_BY_CHAIN: Record<number, string> = {
  1: '0xf44399a74ee5ddef7fa3d064cf66b011ee4a6cae',
  56: '0x298d2967588b5c93a137ce1a05d0b8cfffb3c120',
  43114: '0x4559605e3003fda8c059e14af4f16ba9a004335a',
  8453: '0x4559605e3003fda8c059e14af4f16ba9a004335a',
  11155111: '0xdfdff939d728585ce8a2cf2d4166f043d917d8d2',
};

const ERC20_ABI = [
  'function approve(address spender, uint256 amount) external returns (bool)',
];

const STREAM_FACTORY_ABI = [
  'function createStream((bytes16 streamId,address rewardToken,uint256 netTotalAmount,uint256 feeAmount) params,uint40 deadline,bytes signature) external returns (address stream)',
];

function byteArrayToBytes16(value: number[]): string {
  if (value.length !== 16) {
    throw new Error('Expected txParams.params.params.StreamId to contain 16 bytes');
  }

  return ethers.hexlify(Uint8Array.from(value));
}

function base64ToBytes(value: string): Uint8Array {
  return Uint8Array.from(atob(value), (char) => char.charCodeAt(0));
}

const response = await fetch('https://earn.turtle.xyz/v1/streams/', {
  method: 'POST',
  headers: {
    'X-API-Key': process.env.TURTLE_SECRET_KEY!,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    walletAddress: '0x1111111111111111111111111111111111111111',
    type: 2,
    rewardTokenId: '56b0fab0-5c3e-49f6-a0a7-57e38d5ea999',
    totalAmount: '2500000000000000000000',
    startTimestamp: '2026-03-20T00:00:00Z',
    endTimestamp: '2026-04-20T00:00:00Z',
    customArgs: {
      targetTokenId: '8cc2ed9d-bd59-42fd-9df5-329fa22497b6',
      apr: '0.12',
    },
    adapters: [],
  }),
});

const { txParams } = await response.json();

if (!txParams) {
  throw new Error('Expected txParams for a token-based stream');
}

const streamFactoryAddress = STREAM_FACTORY_BY_CHAIN[txParams.chainId];

if (!streamFactoryAddress) {
  throw new Error(`Unsupported StreamFactory for chainId ${txParams.chainId}`);
}

const provider = new ethers.BrowserProvider(window.ethereum);
await provider.send('eth_requestAccounts', []);

const signer = await provider.getSigner();
const signerAddress = await signer.getAddress();
const connectedChainId = Number((await provider.getNetwork()).chainId);

if (signerAddress.toLowerCase() !== txParams.sender.toLowerCase()) {
  throw new Error(`Connected wallet ${signerAddress} does not match txParams.sender ${txParams.sender}`);
}

if (connectedChainId !== txParams.chainId) {
  throw new Error(`Connected chain ${connectedChainId} does not match txParams.chainId ${txParams.chainId}`);
}

const requiredAllowance =
  BigInt(txParams.params.params.NetTotalAmount) + BigInt(txParams.params.params.FeeAmount);

const rewardToken = new ethers.Contract(
  txParams.params.params.RewardToken,
  ERC20_ABI,
  signer,
);

const approveTx = await rewardToken.approve(streamFactoryAddress, requiredAllowance);
await approveTx.wait();

const streamFactory = new ethers.Contract(
  streamFactoryAddress,
  STREAM_FACTORY_ABI,
  signer,
);

const createStreamTx = await streamFactory.createStream(
  {
    streamId: byteArrayToBytes16(txParams.params.params.StreamId),
    rewardToken: txParams.params.params.RewardToken,
    netTotalAmount: txParams.params.params.NetTotalAmount,
    feeAmount: txParams.params.params.FeeAmount,
  },
  txParams.params.deadline,
  base64ToBytes(txParams.params.signature),
);

const receipt = await createStreamTx.wait();

console.log('Stream creation tx hash:', receipt?.hash);
```

## Operational Notes

<AccordionGroup>
  <Accordion title="Token-based streams are not broadcast automatically">
    The endpoint does not submit the transaction to the chain. It returns the payload required to finalize creation through the `StreamFactory` contract. See [Broadcast the Token-Based Transaction](#broadcast-the-token-based-transaction) for the chain addresses and a TypeScript example. When the wallet submits that transaction, the configured `totalAmount` is part of the on-chain flow, so the wallet must already hold those funds.
  </Accordion>

  <Accordion title="A valid API key alone is not enough">
    This endpoint requires two things at the same time: a valid `X-API-Key` header and the `organization:incentivize:streams:create` permission on the organization attached to that key. If the organization has not been granted that permission, stream creation will be rejected.
  </Accordion>

  <Accordion title="Point-based streams are created immediately">
    Point-based streams do not require an on-chain deployment step, so `txParams` is `null` and the stream is created directly in Turtle's backend. In that case, the returned `stream` is already confirmed.
  </Accordion>

  <Accordion title="Token-based streams are returned in pending state first">
    For token-based streams, the response already includes a persisted `stream` object, but it is still pending until the wallet broadcasts the returned `txParams` to the `StreamFactory`. Until that happens, fields such as `stream.contractAddress` and `stream.creationConfirmedAt` can remain `null`.
  </Accordion>

  <Accordion title="Timestamp alignment is strict">
    `startTimestamp` and `endTimestamp` must be aligned to 15-minute intervals such as `00:00`, `00:15`, `00:30`, or `00:45` UTC.
  </Accordion>

  <Accordion title="Exactly one reward source must be provided">
    `rewardToken` and `pointId` are mutually exclusive. You must specify exactly one of them:

    * `rewardTokenId` for token-based streams
    * `pointId` for point-based streams

    Do not provide both, and do not omit both.
  </Accordion>

  <Accordion title="Points are not part of token-based stream creation">
    If you are creating a stream with `rewardTokenId`, you do not need a point and must not send `pointId`. Points only apply when the reward source is organization-defined points.
  </Accordion>
</AccordionGroup>

## Error Handling

<AccordionGroup>
  <Accordion title="Missing or invalid API key">
    **Status Code:** 401 Unauthorized

    ```json theme={null}
    {
      "error": "Invalid API key"
    }
    ```

    **Solution:** Pass a valid `X-API-Key` header.
  </Accordion>

  <Accordion title="Permission denied">
    **Status Code:** 403 Forbidden

    ```json theme={null}
    {
      "error": {
        "status": "PERMISSION_DENIED",
        "error": "permission denied"
      }
    }
    ```

    **Solution:** Use an API key associated with an organization that has the `organization:incentivize:streams:create` permission.
  </Accordion>

  <Accordion title="Invalid payload">
    **Status Code:** 400 Bad Request

    ```json theme={null}
    {
      "error": {
        "status": "INVALID_ARGUMENT",
        "error": "exactly one between RewardToken and PointID must be provided"
      }
    }
    ```

    **Common causes:**

    * unsupported `type`
    * invalid `customArgs` for the chosen type
    * timestamps not aligned to 15-minute boundaries
    * `totalAmount` present for variable-rate streams
    * `walletAddress` or `rewardTokenId` missing for token-based streams
  </Accordion>

  <Accordion title="Unexpected internal error">
    **Status Code:** 500 Internal Server Error

    **Solution:** Retry the request and contact Turtle if the issue persists.
  </Accordion>
</AccordionGroup>
