Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.portalhq.io/llms.txt

Use this file to discover all available pages before exploring further.

The current Solana alert webhooks supersede the legacy ones. They change the delivery model from “one event per transaction” to “one event per address involved”, and introduce a new event type for delegated transfers along with two new payload fields (triggeredBy and direction). This guide is for customers still consuming the legacy Solana alerts who want to migrate.

What’s changing

  • Per-address delivery. The legacy webhook sent a single event per Solana transaction. The new webhook sends one event per subscribed address involved in the transaction. If two of your Portal clients are involved in a single Solana transaction (e.g. one sends SOL to the other), you will now receive two webhook events for that transaction — each with a different triggeredBy value and the same signature.
  • New event types. Each legacy event type has a replacement, plus a brand-new event for delegated transfers:
    • SOLANA_TX_V2 — Solana native (SOL) transfers and SPL token transfers. (Replaces SOLANA_TX_V1, without delegated transfers.)
    • SOLANA_APPROVE_V2 — Solana delegation approve transactions. (Replaces SOLANA_APPROVAL_V1.)
    • SOLANA_REVOKE_V2 — Solana delegation revoke transactions. (Replaces SOLANA_REVOKE_V1.)
    • SOLANA_DELEGATED_TRANSFER_V2New event. Fires when a delegate executes a transfer using funds from another user’s previously-approved delegation.
    If you don’t subscribe to SOLANA_DELEGATED_TRANSFER_V2, you will not be notified when a delegate moves tokens that one of your addresses had previously approved. To preserve parity with the legacy “every transfer involving my addresses” behavior, subscribe to both SOLANA_TX_V2 and SOLANA_DELEGATED_TRANSFER_V2.
  • triggeredBy field. Every event payload now includes a triggeredBy field. It identifies which of your subscribed addresses caused this specific event delivery. Use this to route events to the right user/account on your side without re-deriving it from the raw transaction data.
  • direction field. Every event payload also includes a direction field that describes what the triggeredBy address did in the transaction:
    ValueMeaning
    OUTBOUNDtriggeredBy is the sender of the transaction — the one who sent native or non-native tokens, who approved or revoked a delegation, or who executed a delegated transfer.
    INBOUNDtriggeredBy is the receiver of the transaction — the one who received tokens, or who was granted approval to spend delegated tokens.
    DELEGATEDOnly on SOLANA_DELEGATED_TRANSFER_V2 events: triggeredBy is the owner whose tokens are being moved by the delegate (the source of funds in the delegated transfer, distinct from the delegate that executed it).

Payload differences

The same Solana transfer (a 0.01 SOL receive) shows up like this on the legacy webhook vs the new one. Note the new triggeredBy and direction fields, and the type value changing from SOLANA_TX_V1 to SOLANA_TX_V2.
{
  "data": {
    "rawEvents": [
      {
        "fee": 80000,
        "slot": 379135212,
        "type": "TRANSFER",
        "events": {},
        "source": "SYSTEM_PROGRAM",
        "feePayer": "75ZfLXXsSpycDvHTQuHnGQuYgd2ihb6Bu4viiCCQ7P4H",
        "signature": "5hyWP82Fn951zjo5DHWAvAC8NjwU7LAKqR3Rpn7sB1H159koer8BefYSSCRuC7X3xVy6rHvMzM7sYHLBwxTAEeUY",
        "timestamp": 1746565669,
        "accountData": [
          {
            "account": "75ZfLXXsSpycDvHTQuHnGQuYgd2ihb6Bu4viiCCQ7P4H",
            "nativeBalanceChange": -10080000,
            "tokenBalanceChanges": []
          },
          {
            "account": "67vPQk4SMRRmZdcfyRd6kBggyeca6TqWmsAkJatNDHyx",
            "nativeBalanceChange": 10000000,
            "tokenBalanceChanges": []
          }
        ],
        "description": "75ZfLXXsSpycDvHTQuHnGQuYgd2ihb6Bu4viiCCQ7P4H transferred 0.01 SOL to 67vPQk4SMRRmZdcfyRd6kBggyeca6TqWmsAkJatNDHyx.",
        "tokenTransfers": [],
        "nativeTransfers": [
          {
            "amount": 10000000,
            "toUserAccount": "67vPQk4SMRRmZdcfyRd6kBggyeca6TqWmsAkJatNDHyx",
            "fromUserAccount": "75ZfLXXsSpycDvHTQuHnGQuYgd2ihb6Bu4viiCCQ7P4H"
          }
        ],
        "transactionError": null
      }
    ]
  },
  "metadata": {
    "isDevnet": true,
    "eventTypes": ["TRANSFER"],
    "environmentId": "environmentId"
  },
  "type": "SOLANA_TX_V1"
}

Event payloads

Below is a full example payload for each new event type so you can wire up parsers and types ahead of cutting over.

SOLANA_TX_V2

{
  "data": {
    "rawEvents": [
      {
        "fee": 80000,
        "slot": 379135212,
        "type": "TRANSFER",
        "events": {},
        "source": "SYSTEM_PROGRAM",
        "feePayer": "75ZfLXXsSpycDvHTQuHnGQuYgd2ihb6Bu4viiCCQ7P4H",
        "signature": "5hyWP82Fn951zjo5DHWAvAC8NjwU7LAKqR3Rpn7sB1H159koer8BefYSSCRuC7X3xVy6rHvMzM7sYHLBwxTAEeUY",
        "timestamp": 1746565669,
        "triggeredBy": "67vPQk4SMRRmZdcfyRd6kBggyeca6TqWmsAkJatNDHyx",
        "direction": "INBOUND",
        "accountData": [
          {
            "account": "75ZfLXXsSpycDvHTQuHnGQuYgd2ihb6Bu4viiCCQ7P4H",
            "nativeBalanceChange": -10080000,
            "tokenBalanceChanges": []
          },
          {
            "account": "67vPQk4SMRRmZdcfyRd6kBggyeca6TqWmsAkJatNDHyx",
            "nativeBalanceChange": 10000000,
            "tokenBalanceChanges": []
          },
          {
            "account": "11111111111111111111111111111111",
            "nativeBalanceChange": 0,
            "tokenBalanceChanges": []
          },
          {
            "account": "ComputeBudget111111111111111111111111111111",
            "nativeBalanceChange": 0,
            "tokenBalanceChanges": []
          }
        ],
        "description": "75ZfLXXsSpycDvHTQuHnGQuYgd2ihb6Bu4viiCCQ7P4H transferred 0.01 SOL to 67vPQk4SMRRmZdcfyRd6kBggyeca6TqWmsAkJatNDHyx.",
        "instructions": [
          {
            "data": "3b1H8Rq1T3d1",
            "accounts": [],
            "programId": "ComputeBudget111111111111111111111111111111",
            "innerInstructions": []
          },
          {
            "data": "LKoyXd",
            "accounts": [],
            "programId": "ComputeBudget111111111111111111111111111111",
            "innerInstructions": []
          },
          {
            "data": "3Bxs4NN8M2Yn4TLb",
            "accounts": [
              "75ZfLXXsSpycDvHTQuHnGQuYgd2ihb6Bu4viiCCQ7P4H",
              "67vPQk4SMRRmZdcfyRd6kBggyeca6TqWmsAkJatNDHyx"
            ],
            "programId": "11111111111111111111111111111111",
            "innerInstructions": []
          }
        ],
        "tokenTransfers": [],
        "nativeTransfers": [
          {
            "amount": 10000000,
            "toUserAccount": "67vPQk4SMRRmZdcfyRd6kBggyeca6TqWmsAkJatNDHyx",
            "fromUserAccount": "75ZfLXXsSpycDvHTQuHnGQuYgd2ihb6Bu4viiCCQ7P4H"
          }
        ],
        "transactionError": null
      }
    ]
  },
  "metadata": {
    "isDevnet": true,
    "eventTypes": ["TRANSFER"],
    "environmentId": "environmentId"
  },
  "type": "SOLANA_TX_V2"
}

SOLANA_APPROVE_V2

{
  "data": {
    "rawEvents": [
      {
        "fee": 10000,
        "slot": 438851116,
        "type": "APPROVE",
        "events": {},
        "source": "SOLANA_PROGRAM_LIBRARY",
        "feePayer": "98aauPkabddWQVyHWNw5LJLDb84ETKfN2xYUy1Zr2sQy",
        "signature": "7fLggf2PBHnH8QfWTzWjjpXKcyUj9h2roR6tCBxAAHJoiLyuYEDx95RqJNz1vnjFSJ8vyaZpanCgEHVLdVvX9Ng",
        "timestamp": 1769837881,
        "triggeredBy": "J4AVMHNUkxhjUmWg25RTfysAEx7FnRYymT2xZkc12sKK",
        "direction": "OUTBOUND",
        "accountData": [
          {
            "account": "98aauPkabddWQVyHWNw5LJLDb84ETKfN2xYUy1Zr2sQy",
            "nativeBalanceChange": -10000,
            "tokenBalanceChanges": []
          },
          {
            "account": "J4AVMHNUkxhjUmWg25RTfysAEx7FnRYymT2xZkc12sKK",
            "nativeBalanceChange": 0,
            "tokenBalanceChanges": []
          },
          {
            "account": "pQxS4DANX5uMkDkzJSk2dSBnrEWZ1enUL2XkKF6Nhbh",
            "nativeBalanceChange": 0,
            "tokenBalanceChanges": []
          },
          {
            "account": "75ZfLXXsSpycDvHTQuHnGQuYgd2ihb6Bu4viiCCQ7P4H",
            "nativeBalanceChange": 0,
            "tokenBalanceChanges": []
          },
          {
            "account": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
            "nativeBalanceChange": 0,
            "tokenBalanceChanges": []
          }
        ],
        "description": "",
        "instructions": [
          {
            "data": "3xHiPX82ia9u",
            "accounts": [
              "pQxS4DANX5uMkDkzJSk2dSBnrEWZ1enUL2XkKF6Nhbh",
              "75ZfLXXsSpycDvHTQuHnGQuYgd2ihb6Bu4viiCCQ7P4H",
              "J4AVMHNUkxhjUmWg25RTfysAEx7FnRYymT2xZkc12sKK"
            ],
            "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
            "innerInstructions": []
          }
        ],
        "tokenTransfers": [],
        "nativeTransfers": [],
        "transactionError": null
      }
    ]
  },
  "metadata": {
    "isDevnet": true,
    "eventTypes": ["APPROVE"],
    "environmentId": "environmentId"
  },
  "type": "SOLANA_APPROVE_V2"
}

SOLANA_REVOKE_V2

{
  "data": {
    "rawEvents": [
      {
        "fee": 10000,
        "slot": 438851202,
        "type": "REVOKE",
        "events": {},
        "source": "SOLANA_PROGRAM_LIBRARY",
        "feePayer": "98aauPkabddWQVyHWNw5LJLDb84ETKfN2xYUy1Zr2sQy",
        "signature": "3p4qVucEtUhZ3GbsQv3ZQS2mTRsziSgJs6B7gre34X2xnG2WPkaNvHuk9wDRJ2kKzKa8M7Rmykpr6wTtYLRuY9oh",
        "timestamp": 1769837914,
        "triggeredBy": "98aauPkabddWQVyHWNw5LJLDb84ETKfN2xYUy1Zr2sQy",
        "direction": "OUTBOUND",
        "accountData": [
          {
            "account": "98aauPkabddWQVyHWNw5LJLDb84ETKfN2xYUy1Zr2sQy",
            "nativeBalanceChange": -10000,
            "tokenBalanceChanges": []
          },
          {
            "account": "J4AVMHNUkxhjUmWg25RTfysAEx7FnRYymT2xZkc12sKK",
            "nativeBalanceChange": 0,
            "tokenBalanceChanges": []
          },
          {
            "account": "pQxS4DANX5uMkDkzJSk2dSBnrEWZ1enUL2XkKF6Nhbh",
            "nativeBalanceChange": 0,
            "tokenBalanceChanges": []
          },
          {
            "account": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
            "nativeBalanceChange": 0,
            "tokenBalanceChanges": []
          }
        ],
        "description": "",
        "instructions": [
          {
            "data": "6",
            "accounts": [
              "pQxS4DANX5uMkDkzJSk2dSBnrEWZ1enUL2XkKF6Nhbh",
              "J4AVMHNUkxhjUmWg25RTfysAEx7FnRYymT2xZkc12sKK"
            ],
            "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
            "innerInstructions": []
          }
        ],
        "tokenTransfers": [],
        "nativeTransfers": [],
        "transactionError": null
      }
    ]
  },
  "metadata": {
    "isDevnet": true,
    "eventTypes": ["REVOKE"],
    "environmentId": "environmentId"
  },
  "type": "SOLANA_REVOKE_V2"
}

SOLANA_DELEGATED_TRANSFER_V2

{
  "data": {
    "rawEvents": [
      {
        "fee": 10000,
        "slot": 413227505,
        "type": "DELEGATED_TRANSFER",
        "events": {},
        "source": "SOLANA_PROGRAM_LIBRARY",
        "feePayer": "2eWxXEVjmyBxaJCPRmZJqtpYm7wkyXfmcNAwFPHBpwLx",
        "signature": "5BFuHM8bMUVfHuwFc4DHoPQsYMbwhYWLGkjv41Lu5dn4wuRb23mK5oDJirjQxKadGYYWzTmSYnUMUed3Reef6Mxx",
        "timestamp": 1776194807,
        "triggeredBy": "9Nmpej98v2kEAXtc4AD52EbQAFrc3ikRe881LehqsEHD",
        "direction": "INBOUND",
        "accountData": [
          {
            "account": "2eWxXEVjmyBxaJCPRmZJqtpYm7wkyXfmcNAwFPHBpwLx",
            "nativeBalanceChange": -10000,
            "tokenBalanceChanges": []
          },
          {
            "account": "9rG4md1zvwpP1NtjMg48HutvuWYaHXerYat2C2E4PAz7",
            "nativeBalanceChange": 0,
            "tokenBalanceChanges": []
          },
          {
            "account": "C2NWfgp2qUb2papQzy28XQPCxeT84zDLSPJRZvXGeZBp",
            "nativeBalanceChange": 0,
            "tokenBalanceChanges": [
              {
                "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
                "userAccount": "49bNwvPHy3krxWLBU3qrK5Gm1RPFqWDXB8xaawcjyCjd",
                "tokenAccount": "C2NWfgp2qUb2papQzy28XQPCxeT84zDLSPJRZvXGeZBp",
                "rawTokenAmount": {
                  "decimals": 6,
                  "tokenAmount": "-1"
                }
              }
            ]
          },
          {
            "account": "C8wu1R39Brrbf8BrKB6uNQbv1pr43RopsQ2LknfdHa6A",
            "nativeBalanceChange": 0,
            "tokenBalanceChanges": [
              {
                "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
                "userAccount": "9Nmpej98v2kEAXtc4AD52EbQAFrc3ikRe881LehqsEHD",
                "tokenAccount": "C8wu1R39Brrbf8BrKB6uNQbv1pr43RopsQ2LknfdHa6A",
                "rawTokenAmount": {
                  "decimals": 6,
                  "tokenAmount": "1"
                }
              }
            ]
          },
          {
            "account": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
            "nativeBalanceChange": 0,
            "tokenBalanceChanges": []
          }
        ],
        "description": "9rG4md1zvwpP1NtjMg48HutvuWYaHXerYat2C2E4PAz7 transferred 0.000001 USDC to 9Nmpej98v2kEAXtc4AD52EbQAFrc3ikRe881LehqsEHD.",
        "instructions": [
          {
            "data": "3DdGGhkhJbjm",
            "accounts": [
              "C2NWfgp2qUb2papQzy28XQPCxeT84zDLSPJRZvXGeZBp",
              "C8wu1R39Brrbf8BrKB6uNQbv1pr43RopsQ2LknfdHa6A",
              "9rG4md1zvwpP1NtjMg48HutvuWYaHXerYat2C2E4PAz7"
            ],
            "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
            "innerInstructions": []
          }
        ],
        "tokenTransfers": [
          {
            "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
            "tokenAmount": 0.000001,
            "toUserAccount": "9Nmpej98v2kEAXtc4AD52EbQAFrc3ikRe881LehqsEHD",
            "tokenStandard": "Fungible",
            "toTokenAccount": "C8wu1R39Brrbf8BrKB6uNQbv1pr43RopsQ2LknfdHa6A",
            "fromUserAccount": "9rG4md1zvwpP1NtjMg48HutvuWYaHXerYat2C2E4PAz7",
            "fromTokenAccount": "C2NWfgp2qUb2papQzy28XQPCxeT84zDLSPJRZvXGeZBp"
          }
        ],
        "nativeTransfers": [],
        "transactionError": null
      }
    ]
  },
  "metadata": {
    "isDevnet": false,
    "eventTypes": ["DELEGATED_TRANSFER"],
    "environmentId": "environmentId"
  },
  "type": "SOLANA_DELEGATED_TRANSFER_V2"
}

Migration steps

  1. Expect multiple events per transaction. If two or more of your subscribed addresses are involved in the same Solana transaction, you will receive one event per address. Update your event handlers to be safe under this fan-out.
    • Idempotency between transactions: dedupe on the signature field. Solana transactions are uniquely identified by their signature, so it works as a stable idempotency key for “have I seen this transaction before?”.
    • Idempotency per (transaction, address): dedupe on the composite key signature + triggeredBy. Use this when you process events per-address, so each (transaction, address) pair is processed exactly once even if the webhook is retried.
  2. Subscribe to the events you need from the Portal Admin Dashboard. In the Alert Webhooks settings, select the event types your integration cares about: SOLANA_TX_V2, SOLANA_APPROVE_V2, SOLANA_REVOKE_V2, and/or SOLANA_DELEGATED_TRANSFER_V2.
    If you want to keep parity with the legacy webhook — i.e. be notified about every transfer involving your addresses — subscribe to both SOLANA_TX_V2 and SOLANA_DELEGATED_TRANSFER_V2. Without SOLANA_DELEGATED_TRANSFER_V2, transfers executed by a delegate against an approval that one of your addresses granted will not generate any webhook event.
  3. Test that events are being received with the correct types. Send a test transaction (devnet works well) and confirm:
    • You receive an event whose type ends in _V2 (not _V1).
    • The payload contains both triggeredBy and direction.
    • When two of your subscribed addresses are involved in the same transaction, you receive two events with different triggeredBy values and a shared signature.