All requests require an API key via the
X-API-Key header. This endpoint requires a secret key (sk_live_) and must be called from your backend.
See Authentication for details.Overview
POST /v2/streams/{id}/withdrawals signs a withdrawal of the reward tokens a stream holds beyond what it must keep to pay its rewards.
The endpoint does not move any funds. It validates the request against the stream’s on-chain balances, reserves the withdrawal, and returns txParams with the backend-signed authorization that the stream admin wallet must submit to withdrawFunds on the corresponding StreamFactory before the signature expires.
Withdrawals are only available on token-based streams. Point-based streams hold no on-chain funds.
A stream accepts only one admin operation (a withdrawal or a budget increase) at a time. While a signed withdrawal is pending, the stream rejects new ones. Use Get Active Admin Operation to see which operation is blocking a stream, until when, and to recover its
txParams if your client lost them.How Much Can Be Withdrawn
The withdrawable amount is computed from the stream contract’s on-chain balance at request time:
The required amount depends on whether the stream has finished processing:
- Stream still running: the stream must keep its full reward budget (
totalAmount). Only the surplus above that budget can be withdrawn, for example tokens sent directly to the stream contract. - Stream fully processed: once the stream’s last snapshot reaches its
endTimestamp, or the stream has distributed its whole budget, it only needs to keep what it has already distributed. The undistributed leftover becomes withdrawable.
amount, the API withdraws the full max withdrawable amount. If nothing can be withdrawn, the request is rejected.
Endpoint
Parameters
Path Parametersuuid
required
Stream identifier, as returned in
id by Get Streams. The stream must belong to the organization attached to the API key.string
EVM address that receives the withdrawn tokens. Defaults to the stream admin (
stream.admin).string
Amount to withdraw, in the reward token’s smallest unit (for example,
1500000000 is 1,500 USDC with 6 decimals). Must be positive and no greater than the max withdrawable amount. Defaults to the max withdrawable amount.integer
default:300
How long the returned signature stays valid, in seconds. Accepts values from
300 (5 minutes, the default) to 86400 (one day). Raise it when the stream admin is a multisig that needs time to collect owner signatures. The stream accepts no other withdrawal or budget increase until the signature expires, or until this withdrawal executes and its transaction is final.Response Example
Response Fields
string
required
Status message.
object
required
Signed withdrawal payload. Submit it to
withdrawFunds on the StreamFactory for txParams.chainId.integer
required
Chain where the stream lives and where the transaction must be submitted.
string
required
Stream admin wallet. It is the only wallet that can submit the transaction.
string
required
Stream contract address.
string
required
Recipient of the withdrawn tokens.
string
required
Amount to withdraw in the reward token’s smallest unit, in decimal-string form to preserve precision.
string
required
Unique 32-byte value (hex) that makes the signature single-use.
integer
required
Unix timestamp, in seconds, after which the signature is no longer accepted on-chain. Returned as a JSON number.
string
required
EIP-712 signature bytes serialized as base64 in JSON.
Unlike the Create Stream payload, the keys inside
txParams.params.params are camelCase (stream, to, amount), and txParams.params.deadline is a JSON number rather than a string.Broadcast the Withdrawal Transaction
The backend does not return a serialized raw transaction. It returns a signed authorization that the stream admin uses to callwithdrawFunds on the StreamFactory for the stream’s chain. No token approval is needed: the factory moves the tokens out of the stream contract.
The wallet that sends the transaction must:
- match
txParams.sender - be connected to
txParams.chainId - submit the transaction before
txParams.params.deadline
StreamFactory address for txParams.chainId from StreamFactory addresses by chain.
TypeScript example
Request the withdrawal from your backend with the secret key, then passtxParams to the admin wallet:
One Operation at a Time
Each stream accepts a single admin operation in flight. A withdrawal signed by this endpoint, or a budget increase made outside the Earn API, blocks new withdrawals on the same stream until the earlier of:- the signature deadline, plus a one-minute tolerance for chain clock skew
- the moment the transaction that executed it becomes final on the stream’s chain
403 with the time it stops blocking. See Get Active Admin Operation for the exact blockingUntil value and the finality window per chain.
Operational Notes
Withdrawals are not broadcast automatically
Withdrawals are not broadcast automatically
The endpoint does not submit any transaction. It returns the payload required to call
withdrawFunds on the StreamFactory. Until the stream admin submits it, no funds leave the stream contract.Only the stream admin can submit the transaction
Only the stream admin can submit the transaction
The signature is bound to the stream admin (
txParams.sender). Any other wallet calling withdrawFunds with it is rejected by the contract. The to address only decides who receives the tokens.Use a longer expiration for multisig admins
Use a longer expiration for multisig admins
The default signature lifetime of 5 minutes suits a single-key wallet. For a multisig such as a Safe, set
signatureExpirationSeconds up to 86400 so the owners have time to sign. Keep in mind that the stream accepts no other withdrawal or budget increase for that whole window, unless the withdrawal executes and becomes final first.Each signature can be used once
Each signature can be used once
The
salt makes every signature single-use. After the withdrawal executes, submitting the same txParams again reverts on-chain. Request a new withdrawal once the stream is free if you need to withdraw more.The stream must be confirmed on-chain
The stream must be confirmed on-chain
A token-based stream can only be withdrawn from after its creation transaction is confirmed and
stream.contractAddress is set. See Create Stream.Concurrent requests on the same stream
Concurrent requests on the same stream
The stream is locked while a withdrawal request is being processed. A second request for the same stream that arrives at the same time returns
409 Conflict. Wait a few seconds and retry.A valid API key alone is not enough
A valid API key alone is not enough
This endpoint requires three things at the same time: a valid
X-API-Key header, a secret key, and the organization:incentivize:streams permission on the organization attached to that key.Error Handling
Missing or invalid API key
Missing or invalid API key
Status Code: 401 UnauthorizedA key that is present but invalid, inactive, or expired returns
401 as well, with "Invalid API key", "API key is inactive", or "API key has expired".Solution: Pass a valid X-API-Key header.Publishable key used
Publishable key used
Status Code: 403 ForbiddenSolution: Call this endpoint from your backend with a secret key (
sk_live_).Permission denied
Permission denied
Status Code: 403 ForbiddenSolution: Use an API key associated with an organization that has the
organization:incentivize:streams permission.Another operation is blocking the stream
Another operation is blocking the stream
Status Code: 403 ForbiddenSolution: Call Get Active Admin Operation. If the operation has not been submitted yet (
confirmedTxHash is null) and txParams.params.deadline has not passed, submit its txParams. Otherwise, wait until blockingUntil and retry.Invalid request
Invalid request
Status Code: 400 Bad RequestCommon causes:
amountis greater than the max withdrawable amountamountis zero or negative- the stream has nothing to withdraw (
maxWithdrawableAmount is non-positive) - the stream is point-based (
withdrawals are only supported on reward-token streams) signatureExpirationSecondsis outside300–86400
Stream not found
Stream not found
Status Code: 404 Not FoundThis happens when the
id path parameter does not correspond to a stream owned by the organization attached to the API key.Stream busy
Stream busy
Status Code: 409 ConflictSolution: Another request is processing the same stream. Retry after a few seconds.
Rate limit exceeded
Rate limit exceeded
Status Code: 429 Too Many RequestsUsage is metered per key, hourly and monthly. Every response carries the hourly budget in the
X-RateLimit-* headers; keys with a monthly limit also get X-Monthly-* headers. A monthly overrun returns "Monthly usage limit exceeded" instead.Solution: Back off until the window resets — hourly limits also return Retry-After — or ask the Turtle team for a higher limit. See Rate limits.Unexpected internal error
Unexpected internal error
Status Code: 500 Internal Server ErrorSolution: Retry the request and contact Turtle if the issue persists.

