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

# Verify Attribution

> Check that a transaction built by the Turtle API carries a valid on-chain attribution marker

<Note>
  Requires an API key via the `X-API-Key` header. See [API Keys](/sdk/authentication/api-keys).
</Note>

<Note>
  **Verify checks the on-chain marker, not attribution.** It reads the transaction calldata and reports whether it ends with a marker Turtle produced. A `signatureValid: false` result does not by itself mean the deposit is unattributed: the deposits credited to your distributor are the ones listed by [Distributor Activity](/sdk/earn-api/deposits).
</Note>

## Overview

`GET /v2/actions/verify`

Check whether a transaction's calldata carries the attribution marker that the Turtle API or SDK embeds when it builds a deposit. Pass a transaction hash and chain ID, and the endpoint re-derives the attribution data from the on-chain calldata alone: which encoding it found (`schema`), the parsed `metadata` (including `distributorId`, `opportunityId`, the deposited `amount`, and `referralCode`), and whether the marker was produced by Turtle (`signatureValid`).

Use it to debug an integration: to confirm that the transactions you broadcast still carry the marker the API generated, or to inspect what a marker decodes to.

## When verify says false for an attributed deposit

Turtle's ledger credits some deposits whose transaction carries no on-chain marker at all. Verify has nothing to read in those cases and returns `200` with `signatureValid: false` and an `error` saying no attribution was found in the calldata. That response says nothing about whether the deposit is credited. It happens for:

* **Manual backfills** entered by a Turtle operator.
* **Intent or off-chain order deposits**, where there is no calldata to stamp.
* **Deposits with your own calldata.** The client called [Deposit](/sdk/earn/deposit) with a `referralCode` but broadcast its own transaction, or the wallet rewrote the calldata before sending it.
* **Transactions that predate tracking**, sent before Turtle started stamping deposits.

A deposit in any of these groups still appears in [Distributor Activity](/sdk/earn-api/deposits) once the ledger has it verified. Verify keeps returning `signatureValid: false` for it, because there is no marker to check.

## Encodings

Verify recognizes two encodings and reports which one it found in the `schema` field.

| `schema` | Marker | Used for | `tag` | `metadata` source | `signatureValid` is `true` when |
| - | - | - | - | - | - |
| `erc8021` | ERC-8021 Schema 2 suffix appended to the calldata | Every deposit generated by the API since 2026-07-20 (current format) | Not present | Resolved from the interaction the API minted for the `actionId` in the suffix | The suffix names Turtle's registry and its `actionId` is an interaction the API minted for this same transaction, chain, and wallet |
| `legacy` | Signed tracking blob at the end of the calldata, tagged `🐢Earn` | Deposits generated before the ERC-8021 rollout | `🐢Earn` | Decoded from the blob in the calldata | The blob is signed by Turtle's attribution wallet |

When the calldata ends with neither marker, `schema` is absent and `signatureValid` is `false`.

## Request

<CodeGroup>
  ```bash curl theme={null}
  curl -X GET "https://earn.turtle.xyz/v2/actions/verify?chainId=1&txHash=0xedfdf71e..." \
    -H "X-API-Key: pk_live_xxxxx"
  ```

  ```typescript TypeScript theme={null}
  const params = new URLSearchParams({
    chainId: '1',
    txHash: '0xedfdf71e0e4daec5afcb87988821a8bccd5c282647c8e81a65a71181a44a8e51',
  });

  const response = await fetch(
    `https://earn.turtle.xyz/v2/actions/verify?${params}`,
    { headers: { 'X-API-Key': 'pk_live_xxxxx' } }
  );
  const data = await response.json();
  ```
</CodeGroup>

**Query Parameters**

<ParamField query="chainId" type="integer" required>
  The chain ID where the transaction was executed (e.g., `1` for Ethereum, `42161` for Arbitrum).
</ParamField>

<ParamField query="txHash" type="string" required>
  The transaction hash to verify (`0x` followed by 64 hex characters).
</ParamField>

## Response

**ERC-8021 (current format)**

There is no `tag` under this schema. The `metadata` comes from the interaction the API minted when it built the deposit, keyed by the `actionId` carried in the suffix.

```json theme={null}
{
  "schema": "erc8021",
  "metadata": {
    "action": "deposit",
    "actionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "amount": "1000000",
    "amountUsd": "1.00",
    "distributorId": "dist123",
    "opportunityId": "550e8400-e29b-41d4-a716-446655440000",
    "referralCode": "ref456",
    "tokenIn": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
    "tokenInDecimals": 6
  },
  "signatureValid": true
}
```

**Legacy**

Transactions built before the ERC-8021 rollout carry the signed tracking blob. The `tag` is present and the `metadata` is decoded from the calldata itself.

```json theme={null}
{
  "schema": "legacy",
  "tag": "🐢Earn",
  "metadata": {
    "action": "deposit",
    "actionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "amount": "1000000",
    "amountUsd": "1.00",
    "distributorId": "dist123",
    "opportunityId": "550e8400-e29b-41d4-a716-446655440000",
    "referralCode": "ref456",
    "tokenIn": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
    "tokenInDecimals": 6
  },
  "signatureValid": true
}
```

**No marker**

```json theme={null}
{
  "signatureValid": false,
  "error": "no attribution found: ..."
}
```

<ResponseField name="schema" type="string">
  Which encoding was found: `erc8021` or `legacy`. Absent when the calldata carries no marker.
</ResponseField>

<ResponseField name="tag" type="string">
  The raw tracking tag found in the transaction data. Only present under `legacy` (value `🐢Earn`). ERC-8021 markers carry no tag.
</ResponseField>

<ResponseField name="metadata" type="object">
  Attribution metadata for the transaction. Under `legacy` it is decoded from the calldata. Under `erc8021` it is resolved from the interaction the API minted for the `actionId` in the suffix.

  <Expandable title="metadata">
    <ResponseField name="action" type="string">
      The attributed action type (e.g., `deposit`).
    </ResponseField>

    <ResponseField name="actionId" type="string">
      The action ID the API returned when it built the transaction.
    </ResponseField>

    <ResponseField name="amount" type="string">
      The deposited amount in the input token's smallest unit.
    </ResponseField>

    <ResponseField name="amountUsd" type="string">
      The deposited amount in USD.
    </ResponseField>

    <ResponseField name="distributorId" type="string">
      The distributor named by the marker.
    </ResponseField>

    <ResponseField name="opportunityId" type="string">
      The opportunity the deposit was made into.
    </ResponseField>

    <ResponseField name="referralCode" type="string">
      The referral code carried by the marker, if present.
    </ResponseField>

    <ResponseField name="tokenIn" type="string">
      The address of the input token.
    </ResponseField>

    <ResponseField name="tokenInDecimals" type="integer">
      The decimals of the input token.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="signatureValid" type="boolean" required>
  Whether the marker was produced by Turtle. Under `legacy`: the blob is signed by Turtle's attribution wallet. Under `erc8021`: the suffix names Turtle's registry and its `actionId` is an interaction the API minted for this same transaction, chain, and wallet. `false` when the marker fails that check or when there is no marker at all. It is not an attribution verdict.
</ResponseField>

<ResponseField name="error" type="string">
  Explains why `signatureValid` is `false` (e.g., no marker found in the calldata, or an ERC-8021 `actionId` that this API did not mint). Absent on a valid marker.
</ResponseField>

## Error Handling

<AccordionGroup>
  <Accordion title="Malformed txHash">
    **Status:** 400 Bad Request

    ```json theme={null}
    {
      "status": "INVALID_ARGUMENT",
      "error": "invalid argument: invalid transaction hash format"
    }
    ```

    **Solution:** Pass a `txHash` of `0x` followed by 64 hex characters.
  </Accordion>

  <Accordion title="Missing or non-integer chainId">
    **Status:** 400 Bad Request

    ```json theme={null}
    {
      "status": "INVALID_ARGUMENT",
      "error": "bad request",
      "context": {
        "query:chainId": ["missing value"]
      }
    }
    ```

    The `context` entry is keyed by the failing parameter (`query:chainId`) and carries the validator's message.

    **Solution:** Pass `chainId` as an integer for a supported chain.
  </Accordion>

  <Accordion title="Transaction not found or RPC unavailable">
    **Status:** 200 OK

    ```json theme={null}
    {
      "signatureValid": false,
      "error": "transaction not found"
    }
    ```

    The endpoint never returns 404 for an unknown transaction. RPC and parse failures (`transaction not found`, `unsupported chain or RPC unavailable`) come back as `200` with `signatureValid: false` and the reason in `error`.

    **Solution:** Confirm the transaction is mined on the chain you passed as `chainId`, then retry.
  </Accordion>

  <Accordion title="No marker in the calldata">
    **Status:** 200 OK

    ```json theme={null}
    {
      "signatureValid": false,
      "error": "no attribution found: ..."
    }
    ```

    **Solution:** This is the expected result for a transaction the API did not build (or whose calldata was rewritten) and for backfilled, intent-based, or pre-tracking deposits. It does not mean the deposit is unattributed; check [Distributor Activity](/sdk/earn-api/deposits) for the ledger's view. If you expected the API-built calldata to be broadcast unchanged, compare the transaction's `data` with what [Deposit](/sdk/earn/deposit) returned.
  </Accordion>

  <Accordion title="Marker not produced by Turtle">
    **Status:** 200 OK

    ```json theme={null}
    {
      "schema": "erc8021",
      "signatureValid": false,
      "error": "ERC-8021 actionId was not minted by this API"
    }
    ```

    **Solution:** A marker is present but Turtle did not produce it. Under `erc8021` the `error` names the failed check: the suffix names a registry that is not Turtle's, the `actionId` was not minted by this API, or it was minted for a different transaction or wallet. Under `legacy` the blob is signed by a key other than Turtle's attribution wallet; the response then carries `tag` and `metadata` with `signatureValid: false`. The verification itself succeeded, so the status is `200`. Rebuild the deposit through [Deposit](/sdk/earn/deposit) so the API stamps it.
  </Accordion>
</AccordionGroup>

## Related

* [Deposit](/sdk/earn/deposit): Generate the deposit whose marker you are verifying
* [Distributor Activity](/sdk/earn-api/deposits): List every deposit attributed to your distributor
* [Distributor Model](/sdk/concepts/distributor-model): How attribution and revenue tracking work


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