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

# Get Active Admin Operation

> See which withdrawal or budget increase is blocking a stream, and recover its signed payload

<Note>
  All requests require an API key via the `X-API-Key` header. This endpoint requires a secret key (`sk_live_`) even though it is a read, because the response contains a signed payload. Call it from your backend.
  See [Authentication](/sdk/authentication/api-keys) for details.
</Note>

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

## Overview

`GET /v2/streams/{id}/admin-operations/active` returns the admin operation currently blocking a token-based stream, and until when it blocks it.

An admin operation is a backend-signed change to a stream's funds that the stream admin submits to the `StreamFactory`. There are two kinds today:

* a **withdrawal**, signed by [Withdraw Funds](/sdk/streams/withdraw-funds)
* a **budget increase**, made outside the Earn API

A stream accepts only one admin operation at a time, so the response tells you whether the stream can take a new one. Use this endpoint to:

* **Recover a lost payload.** If your client lost the `txParams` of a signed operation (a closed browser, a dismissed wallet prompt), the endpoint returns the same payload. Nothing is re-signed, so it grants nothing the original signature did not.
* **Check whether the operation executed.** Once `confirmedTxHash` is set, the transaction landed on-chain and must not be submitted again.
* **Know when the stream frees up.** `blockingUntil` is when the stream accepts a new withdrawal or budget increase.

## Endpoint

<CodeGroup>
  ```bash curl theme={null}
  curl -X GET "https://earn.turtle.xyz/v2/streams/550e8400-e29b-41d4-a716-446655440000/admin-operations/active" \
    -H "X-API-Key: sk_live_xxxxx"
  ```

  ```typescript TypeScript theme={null}
  const streamId = '550e8400-e29b-41d4-a716-446655440000';

  const response = await fetch(
    `https://earn.turtle.xyz/v2/streams/${streamId}/admin-operations/active`,
    { headers: { 'X-API-Key': process.env.TURTLE_SECRET_KEY! } }
  );

  const { operation } = await response.json();
  ```
</CodeGroup>

## Parameters

**Path Parameters**

<ParamField path="id" type="uuid" required>
  Stream identifier, as returned in `id` by [Get Streams](/sdk/streams/get-streams). The stream must be token-based and belong to the organization attached to the API key.
</ParamField>

## Response Examples

<CodeGroup>
  ```json Pending withdrawal theme={null}
  {
    "operation": {
      "type": "withdraw",
      "txParams": {
        "chainId": 8453,
        "sender": "0x1111111111111111111111111111111111111111",
        "params": {
          "params": {
            "stream": "0x2222222222222222222222222222222222222222",
            "to": "0x1111111111111111111111111111111111111111",
            "amount": "1500000000"
          },
          "salt": "0x3f9a6c2e1b7d4f8a0c5e9b2d6f1a4c8e7b3d0f5a9c2e6b1d4f8a7c3e0b5d9f2a",
          "deadline": 1790859600,
          "signature": "MEUCIA...base64..."
        }
      },
      "blockingUntil": "2026-10-01T13:01:00Z",
      "confirmedAt": null,
      "confirmedTxHash": null
    }
  }
  ```

  ```json Executed budget increase theme={null}
  {
    "operation": {
      "type": "add_funds",
      "txParams": {
        "chainId": 1,
        "sender": "0x1111111111111111111111111111111111111111",
        "params": {
          "params": {
            "stream": "0x3333333333333333333333333333333333333333",
            "netAmount": "10000000000",
            "feeAmount": "150000000"
          },
          "salt": "0x8d2b5f9e1c4a7d3b6e0f2a9c5d8b1e4f7a3c6d9e2b5f8a1c4d7e0b3f6a9c2d5e",
          "deadline": 1790859600,
          "signature": "MEUCIA...base64..."
        }
      },
      "blockingUntil": "2026-10-01T12:35:00Z",
      "confirmedAt": "2026-10-01T12:20:00Z",
      "confirmedTxHash": "0x9c4e7a1d3b6f8e2c5a0d7b4f1e8c3a6d9b2f5e8a1c4d7b0e3f6a9c2d5b8e1f4a"
    }
  }
  ```

  ```json No active operation theme={null}
  {
    "operation": null
  }
  ```
</CodeGroup>

```typescript theme={null}
operation: ActiveAdminOperation | null
```

## Response Fields

<ResponseField name="operation" type="ActiveAdminOperation | null" required>
  The admin operation blocking the stream. `null` when the stream accepts a new withdrawal or budget increase.
</ResponseField>

### `ActiveAdminOperation`

<ResponseField name="type" type="string" required>
  Which `StreamFactory` function `txParams` calls, and therefore the shape of `txParams.params.params`: `withdraw` (`withdrawFunds`) or `add_funds` (`addFunds`).
</ResponseField>

<ResponseField name="txParams" type="object" required>
  Signed payload of the operation, exactly as it was issued.
</ResponseField>

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

<ResponseField name="txParams.sender" type="string" required>
  Stream admin wallet. It is the only wallet that can submit the transaction.
</ResponseField>

<ResponseField name="txParams.params.params" type="object" required>
  Signed arguments of the `StreamFactory` call. Exactly one of two shapes, selected by `type`:

  * `withdraw`: `stream`, `to`, and `amount`, all present
  * `add_funds`: `stream`, `netAmount`, and `feeAmount`, all present

  The two shapes never mix: a `withdraw` payload carries no `netAmount` or `feeAmount`, and an `add_funds` payload carries no `to` or `amount`.
</ResponseField>

<ResponseField name="txParams.params.params.stream" type="string" required>
  Stream contract address. Present for both types.
</ResponseField>

<ResponseField name="txParams.params.params.to" type="string">
  `withdraw` only. Recipient of the withdrawn tokens.
</ResponseField>

<ResponseField name="txParams.params.params.amount" type="string">
  `withdraw` only. Amount to withdraw in the reward token's smallest unit.
</ResponseField>

<ResponseField name="txParams.params.params.netAmount" type="string">
  `add_funds` only. Budget increase in the reward token's smallest unit.
</ResponseField>

<ResponseField name="txParams.params.params.feeAmount" type="string">
  `add_funds` only. Fee charged on top of `netAmount`, in the reward token's smallest unit.
</ResponseField>

<ResponseField name="txParams.params.salt" type="string" required>
  Unique 32-byte value (hex) that makes the signature single-use.
</ResponseField>

<ResponseField name="txParams.params.deadline" type="integer" required>
  Unix timestamp, in seconds, after which the signature is no longer accepted on-chain. Returned as a JSON number.
</ResponseField>

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

<ResponseField name="blockingUntil" type="datetime" required>
  When the stream accepts a new withdrawal or budget increase again. See [When the stream frees up](#when-the-stream-frees-up).
</ResponseField>

<ResponseField name="confirmedAt" type="datetime | null" required>
  Block time of the transaction that executed the operation. `null` until it executes.
</ResponseField>

<ResponseField name="confirmedTxHash" type="string | null" required>
  Hash of the transaction that executed the operation. `null` until it executes. Once set, do not submit `txParams` again.
</ResponseField>

## Operation Lifecycle

| State | How to recognize it | What to do |
| - | - | - |
| Signed, not submitted | `confirmedTxHash` is `null` and `txParams.params.deadline` is in the future | Submit `txParams` before `txParams.params.deadline` |
| Expired without executing | `confirmedTxHash` is `null` and `txParams.params.deadline` has passed | Nothing; the stream frees up at `blockingUntil` |
| Executed, not yet final | `confirmedTxHash` is set | Do not submit again; the stream frees up at `blockingUntil` |
| No operation | `operation` is `null` | The stream accepts a new withdrawal or budget increase |

### When the stream frees up

`blockingUntil` is the earlier of:

* `txParams.params.deadline` plus a one-minute tolerance for chain clock skew. After that, the signature can no longer be used.
* `confirmedAt` plus the finality window of the stream's chain. After that, the executed transaction can no longer be reverted by a reorg.

| `chainId` | Network | Finality window |
| - | - | - |
| `1` | Ethereum | 15 minutes |
| `56` | BSC | 1 minute |
| `43114` | Avalanche | 1 minute |
| `8453` | Base | 30 minutes |
| `11155111` | Sepolia | 15 minutes |

For example, an Ethereum operation signed with a one-hour expiration and executed 20 minutes later frees the stream 15 minutes after execution, well before its deadline.

## Resume a Pending Operation

Read the operation from your backend, then let the stream admin wallet submit it according to `type`. The `StreamFactory` address for each chain is listed in [StreamFactory addresses by chain](/sdk/streams/create-stream#streamfactory-addresses-by-chain).

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

const STREAM_FACTORY_ABI = [
  'function withdrawFunds((address stream,address to,uint256 amount) params,bytes32 salt,uint40 deadline,bytes signature) external',
  'function addFunds((address stream,uint256 netAmount,uint256 feeAmount) params,bytes32 salt,uint40 deadline,bytes signature) external',
];

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

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

// Backend: decide whether there is anything to submit.
const response = await fetch(
  `https://earn.turtle.xyz/v2/streams/${streamId}/admin-operations/active`,
  { headers: { 'X-API-Key': process.env.TURTLE_SECRET_KEY! } }
);
const { operation } = await response.json();

const canSubmit =
  operation !== null &&
  operation.confirmedTxHash === null &&
  operation.txParams.params.deadline > Date.now() / 1000;

// Wallet: the stream admin (operation.txParams.sender) on operation.txParams.chainId.
// streamFactoryAddress is the StreamFactory for operation.txParams.chainId.
// rewardTokenAddress is the stream's rewardToken.address from Get Streams.
if (canSubmit) {
  const { params, salt, deadline, signature } = operation.txParams.params;
  const streamFactory = new ethers.Contract(streamFactoryAddress, STREAM_FACTORY_ABI, signer);

  let tx;
  switch (operation.type) {
    case 'withdraw':
      tx = await streamFactory.withdrawFunds(
        { stream: params.stream, to: params.to, amount: params.amount },
        salt,
        deadline,
        base64ToBytes(signature),
      );
      break;

    case 'add_funds': {
      // addFunds pulls netAmount + feeAmount of the reward token from the admin wallet.
      const rewardToken = new ethers.Contract(rewardTokenAddress, ERC20_ABI, signer);
      const approveTx = await rewardToken.approve(
        streamFactoryAddress,
        BigInt(params.netAmount) + BigInt(params.feeAmount),
      );
      await approveTx.wait();

      tx = await streamFactory.addFunds(
        { stream: params.stream, netAmount: params.netAmount, feeAmount: params.feeAmount },
        salt,
        deadline,
        base64ToBytes(signature),
      );
      break;
    }

    default:
      throw new Error(`Unsupported admin operation type: ${operation.type}`);
  }

  await tx.wait();
}
```

See [Broadcast the Withdrawal Transaction](/sdk/streams/withdraw-funds#broadcast-the-withdrawal-transaction) for the wallet and chain checks to run before submitting.

## Operational Notes

<AccordionGroup>
  <Accordion title="Nothing is re-signed">
    The endpoint returns the payload that was signed when the operation was created: same parameters, same `salt`, same `deadline`. Calling it repeatedly never extends the deadline or creates a new signature.
  </Accordion>

  <Accordion title="Budget increases also block withdrawals">
    A budget increase made outside the Earn API blocks the stream exactly like a withdrawal does. It shows up here with `type: "add_funds"`, and [Withdraw Funds](/sdk/streams/withdraw-funds) rejects new requests until its `blockingUntil`.
  </Accordion>

  <Accordion title="Confirmation is detected on-chain">
    `confirmedAt` and `confirmedTxHash` are set when Turtle detects the executing transaction on-chain. They can lag a few moments behind the transaction receipt your wallet returns.
  </Accordion>

  <Accordion title="Point-based streams have no admin operations">
    Point-based streams hold no on-chain funds, so they cannot be withdrawn from or topped up through the `StreamFactory`. Requesting one returns `400 Bad Request`.
  </Accordion>

  <Accordion title="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.
  </Accordion>
</AccordionGroup>

## Error Handling

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

    ```json theme={null}
    {
      "error": "API key required. Pass it via the X-API-Key header."
    }
    ```

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

  <Accordion title="Publishable key used">
    **Status Code:** 403 Forbidden

    ```json theme={null}
    {
      "error": "This endpoint requires a secret API key"
    }
    ```

    **Solution:** Call this endpoint from your backend with a secret key (`sk_live_`).
  </Accordion>

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

    ```json theme={null}
    {
      "error": {
        "status": "PERMISSION_DENIED",
        "error": "organization lacks required permission: organization:incentivize:streams"
      }
    }
    ```

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

  <Accordion title="Point-based stream">
    **Status Code:** 400 Bad Request

    ```json theme={null}
    {
      "error": {
        "status": "INVALID_ARGUMENT",
        "error": "points streams carry no admin operations"
      }
    }
    ```

    **Solution:** Only request token-based streams.
  </Accordion>

  <Accordion title="Stream not found">
    **Status Code:** 404 Not Found

    ```json theme={null}
    {
      "error": {
        "status": "NOT_FOUND",
        "error": "stream not found"
      }
    }
    ```

    This happens when the `id` path parameter does not correspond to a stream owned by the organization attached to the API key.
  </Accordion>

  <Accordion title="Rate limit exceeded">
    **Status Code:** 429 Too Many Requests

    ```json theme={null}
    {
      "error": "Rate limit exceeded"
    }
    ```

    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](/sdk/authentication/api-keys#rate-limits).
  </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>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.