Crypto-to-Crypto Transactions

Send tokens from your organisation wallet to up to 30 beneficiary wallets in one on-chain batch payment on an EVM network

A crypto-to-crypto transaction (type: CRYPTO) pays tokens to one or more beneficiary wallets. You register the transaction with the API, then your own wallet executes it on-chain by calling the Skyline batch payments contract. Skyline sees the on-chain event and completes the transaction.

There is no quote, no commission and no accept call.

Read Transactions first for statuses, preconditions and errors.

Supported networks: EVM networks. Crypto-to-crypto on Solana is not currently supported through the API.

Prerequisites

  • An organisation wallet holding the token. See Wallets.
  • A beneficiary wallet for each payee, registered with POST /wallets and approved by screening. See Wallets and Receivers.
  • An API user with the authorized role, 2FA off, in an ACTIVE organisation.

Step 1: Look Up the Ids

CallWhat you take from it
GET /networks/client-visibleAn EVM network whose features includes CRYPTO. Keep its _id, chainId and contracts.batchPayments.address.
GET /tokensThe token's isoCode. In its ERC20Address array, the entry whose chainId matches the network gives the token contract address.
GET /receivers/me?paymentType=cryptoReceivers that have wallets, with their wallets. Keep each beneficiary wallet's _id.
GET /users/meselectedOrganisation.wallets: your organisation wallet on that network.

The batch payments contract address is set per network. Always read it from the network object. Do not hardcode it.

Step 2: Create the Transaction

POST /transactions

{
  "type": "CRYPTO",
  "token": "USDC",
  "network": "6650a1f2c3d4e5f6a7b8c9d0",
  "walletAddress": "0x4b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c",
  "payments": [
    {
      "type": "CRYPTO",
      "wallet": "6650c3f4e5a6b7c8d9e0f1a2",
      "tokenIsoCode": "USDC",
      "amount": "1500",
      "reason": "Invoice 88"
    }
  ]
}

Transaction fields:

FieldRequiredDescription
typeYesCRYPTO.
tokenYesThe token isoCode you send.
networkYesThe network _id.
walletAddressYesYour organisation wallet. Must be lowercase. See below.
paymentsYes1 to 30 payments.

Payment fields:

FieldRequiredDescription
typeYesCRYPTO.
walletYesThe beneficiary wallet _id.
tokenIsoCodeYesThe token the beneficiary receives. Use the same value as token.
amountYesThe token amount, as a decimal string.
reasonYesA payment reference.
instructionsNoFree-text instructions.

Send walletAddress in lowercase. Skyline compares the address that sent the on-chain payment, in lowercase, to the stored walletAddress. A checksummed address never matches, and the transaction never completes.

The response is the transaction in DRAFT. tokensToSend is the sum of the payment amounts. There is no commission.

Step 3: Validate

POST /transactions/:id/validate returns 200 with an empty body when the transaction is valid.

Step 4: Check the Allowance and Approve

POST /transactions/:id/check-allowance returns expectedAllowance, amountAllowed, hasAllowance and needsReset, as in Crypto-to-Fiat. For crypto-to-crypto, expectedAllowance is the sum of the payment amounts, with no margin.

If hasAllowance is false, call ERC-20 approve(spender, amount) from your organisation wallet:

  • Spender: the network's contracts.batchPayments.address.
  • Amount: expectedAllowance in base units, using the token's decimals.
  • When needsReset is true: approve 0 first, wait for the receipt, then approve the amount.

Call check-allowance again until hasAllowance is true.

Step 5: Submit

POST /transactions/:id/submit with no body.

The transaction moves to WAITING_ACCEPTANCE. Nothing is priced and nothing expires. The on-chain payment only completes a transaction in this status, so submit before you send it.

Step 6: Verify the Accept Binding

Before your wallet signs anything, confirm the transaction has not changed:

  1. GET /transactions/:id/review-context returns acceptBinding.hash.
  2. POST /transactions/:id/verify-accept-binding:
{
  "acceptBindingHash": "9f2c4e7a1b3d5f6e8a0c2e4f6a8b0d2e4f6a8c0e2a4c6e8f0a2b4d6f8a0c2e4f"
}

It returns 204 when the transaction matches, or 409 ACCEPT_BINDING_MISMATCH when it changed. It changes nothing. It needs the same role and organisation status as accept.

Step 7: Send the Batch Payment On-Chain

From the organisation wallet in walletAddress, call sendBatchPayments on the batch payments contract (contracts.batchPayments.address):

function sendBatchPayments(
    string _transactionId,
    address _token,
    (address to, uint256 amount)[] _payments
) external;
ArgumentValue
_transactionIdThe transaction _id, as a string.
_tokenThe token contract address for this network, from ERC20Address.
_paymentsOne entry per item in the transaction's payments array, as returned by the API: to = payment.wallet.address, amount = payment.amount in base units.

Convert amount to base units with the token's decimals(). For example, 1500 of a 6-decimal token is 1500000000.

Pay exactly what the transaction's payments array says. Read it from GET /transactions/:id just before you build the call.

Do not call POST /transactions/:id/accept. For crypto-to-crypto on EVM it returns 400 Crypto transactions are not allowed in this method and moves the transaction to FAILED. If that happens, call submit again to return it to WAITING_ACCEPTANCE.

Step 8: Track to Completion

The contract emits BatchPaymentSent(string transactionId, address indexed sender). Skyline listens for it and, when sender matches walletAddress, moves the transaction to COMPLETED and records the txHash.

  • Check your own receipt. A reverted transaction emits no event, and the Skyline transaction stays in WAITING_ACCEPTANCE.
  • Poll GET /transactions/:id every 30 to 60 seconds until status is COMPLETED.
  • If the receipt succeeded but the transaction does not complete, check that walletAddress is lowercase and that the call came from that wallet.

Endpoint Reference

EndpointMethodPurpose
/transactionsPOSTCreate the transaction
/transactions/:idPUTReplace the transaction body
/transactions/:id/validatePOSTValidate
/transactions/:id/check-allowancePOSTRead the required and current allowance
/transactions/:id/submitPOSTMove to WAITING_ACCEPTANCE
/transactions/:id/review-contextGETRead the accept binding hash
/transactions/:id/verify-accept-bindingPOSTConfirm nothing changed before signing
/transactions/:idGETRead status

On this page