Skip to main content
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.
To withdraw funds, the organization attached to the API key must have the organization:incentivize:streams permission. If your organization does not have that permission enabled, contact the Turtle team to request it.

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.
If you omit amount, the API withdraws the full max withdrawable amount. If nothing can be withdrawn, the request is rejected.

Endpoint

To withdraw everything that can be withdrawn to the stream admin, send an empty body:

Parameters

Path Parameters
uuid
required
Stream identifier, as returned in id by Get Streams. The stream must belong to the organization attached to the API key.
Request Body
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 call withdrawFunds 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
Use the StreamFactory address for txParams.chainId from StreamFactory addresses by chain.

TypeScript example

Request the withdrawal from your backend with the secret key, then pass txParams to the admin wallet:
If the user closes the page or dismisses the wallet prompt before submitting, do not request a new withdrawal: the stream stays blocked by the first one. Call Get Active Admin Operation to get the same txParams back and submit them before the deadline.

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
This prevents two signed operations from both drawing on the same balance. A request made while another operation is blocking the stream returns 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

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.
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.
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.
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.
A token-based stream can only be withdrawn from after its creation transaction is confirmed and stream.contractAddress is set. See Create 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.
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

Status Code: 401 Unauthorized
A 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.
Status Code: 403 Forbidden
Solution: Call this endpoint from your backend with a secret key (sk_live_).
Status Code: 403 Forbidden
Solution: Use an API key associated with an organization that has the organization:incentivize:streams permission.
Status Code: 403 Forbidden
Solution: 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.
Status Code: 400 Bad Request
Common causes:
  • amount is greater than the max withdrawable amount
  • amount is 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)
  • signatureExpirationSeconds is outside 300–86400
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.
Status Code: 409 Conflict
Solution: Another request is processing the same stream. Retry after a few seconds.
Status Code: 429 Too Many Requests
Usage 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.
Status Code: 500 Internal Server ErrorSolution: Retry the request and contact Turtle if the issue persists.