Allways Access / Developersv1Get your API key
Widgets

Checkout widget

This is the mainnet API. Real funds on every network.

Take crypto payments with one button. Your customer pays in a modal with the asset they hold. You receive the asset you choose in one wallet, and a signed webhook tells you which account paid.

How it works

You set up once. After that, every payment runs the same way.

Set up once

  1. Choose what to receive. Sign in, accept the terms, and open Checkout widget. The first visit opens a guided setup. In Design your widget, save one asset and one wallet.
  2. Paste one snippet. In Add it to your site, select Create key and paste the snippet into your page.
  3. Choose how to be told. A signed webhook is the main path, and polling is the fallback. Send a test payment makes a simulated payment, and tells your webhook when one is saved.

After setup the page has three tabs: Payments, Settings and Developers.

Amounts, pay assets, your name, your colours, your logo and the wording (add funds, pay or donate) come from the dashboard. The page never sets where the money goes: your wallet address lives on our server only.

HTMLhtml
<script src="https://allways.venturalabs.ai/checkout.js"></script>
<button onclick="AllwaysCheckout.open({
  key: 'alw_pub_abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG',
  account: '{{ user.id }}',
  amount: '{{ order.total }}',
  onSuccess: (p) => refreshBalance()
})">Add funds with crypto</button>

Each payment

You never trace a transfer. Allways Access tells you which account paid. The flow shows the default account mode: your page passes account.

Customer: the modal on your site. Allways Access: hosted backend. Allways network: fills the payment. You: wallet and backend. 1. Customer → Allways Access: starts a payment (account = user_877). 2. Allways Access: saves the link (payment ID ↔ user_877). 3. Allways Access → Customer: shows where to pay. 4. Customer → Allways network: sends the pay asset (for example SOL). 5. Allways network → You: sends USDC (to your wallet). 6. Allways Access → You: payment.delivered: user_877 paid 50 USDC (signed webhook, or your poll). 7. You: credit user_877 (in your own system).Customerthe modal on your siteAllways Accesshosted backendAllways networkfills the paymentYouwallet and backendstarts a paymentaccount = user_8771saves the linkpayment ID ↔ user_8772shows where to pay3sends the pay assetfor example SOL4sends USDCto your wallet5payment.delivered: user_877 paid 50 USDCsigned webhook, or your poll6credit user_877in your own system7
  1. Customer → Allways Accessstarts a payment account = user_877
  2. Allways Accesssaves the link payment ID ↔ user_877
  3. Allways Access → Customershows where to pay
  4. Customer → Allways networksends the pay asset for example SOL
  5. Allways network → Yousends USDC to your wallet
  6. Allways Access → Youpayment.delivered: user_877 paid 50 USDC signed webhook, or your poll
  7. Youcredit user_877 in your own system

AllwaysCheckout.open

AllwaysCheckout.open(options) opens the modal. AllwaysCheckout.close() closes it.

OptionValue
keyRequired. Your publishable alw_pub_… key. It is safe in page source. It is the same key as the exchange widget.
accountThe customer's account ID in your system: 1 to 128 characters of A-Z a-z 0-9 . _ : @ + -. Required in site mode. A passed account wins in every mode.
amountOptional. The price in the asset you receive. It must be a positive decimal that the asset supports. When it is valid, the customer cannot change the receive amount.
themeauto (default) follows the customer's device. Set light or dark to match your site.
onSuccessCalled once with { id, account } when the payment is delivered and the modal is open. Display only.
onCloseCalled when the modal closes, paid or not.

onSuccess is display only and is never a credit signal. A customer can call it from the browser console, and it does not fire when the customer closes the modal before the payment completes. Use it to refresh what the page shows. Credit the account from the webhook or the poll.

amount is not signed. A customer can change any browser value. Create the order on your server, then call open({ account: orderId, amount }). Match the signed webhook account to the order. Credit the signed webhook amount, never the browser value. Mark a fixed-price order paid only when amount >= price.

Account modes

One setting in the dashboard decides how a payment names the customer. The name arrives as account in the webhook.

ModeWho names the accountaccount in the webhook
site (default)Your page passes account. The customer types nothing.The value you passed
askThe customer types an email or username first.The value they typed
walletNobody. The paying wallet is the account.The paying address
noneNobody.null

account is not signed. A customer who changes it pays for another account, which costs you nothing. In site mode a page that passes no account shows a "not set up" state.

Webhook

Save an https URL in Developers. We POST one JSON body to it when a payment ends.

The event says where the customer's money ended.

EventThe money isamountYou
payment.deliveredIn your walletWhat to credit, in assetCredit the customer
payment.refundedBack with the customer"0"Do nothing
payment.failedWith neither of you"0"Do nothing. The customer may contact you with the payment ID.

A payment the customer never sent has no event.

amount and settled

Credit amount. settled is what landed in your wallet, for your books.

They differ in one case: the party filling the payment missed its deadline, so its collateral paid in its place, with about 10% on top, in SOL or TAO.

Customer paysYou sell forCollateral landsEventamountsettled
ETH20 SOLIn your walletpayment.delivered"20"{"asset":"sol","amount":"22"}
20 SOLETHIn the customer's walletpayment.refunded"0"null
FieldMeaning
eventpayment.delivered, payment.refunded or payment.failed
idThe payment ID. The same ID arrives again on a retry or a resend.
accountSee account modes. null in none mode.
assetThe asset you receive, an id from GET /v1/currencies
amountA decimal string in asset: what to credit. "0" unless the event is payment.delivered.
settled{ asset, amount }: what landed in your wallet. null unless the event is payment.delivered, and null when the landed amount is not known yet.
txHashThe transaction that paid your wallet. On payment.refunded it is the refund transaction. It can be null when collateral paid in TAO, and it is null on payment.failed.
testtrue only for a payment made with Send test payment

A test payment carries "test": true, the ID test_…, the account test-customer, your first amount and txHash: null. No money moves and no credit is spent.

The webhook body can contain fields that are not listed here. Ignore fields that you do not use.

Responsejson
{
  "event": "payment.delivered",
  "id": "5d1f3c2e-8a1b-4c7d-9e0f-123456789abc",
  "account": "user_877",
  "asset": "solusdc",
  "amount": "50",
  "settled": { "asset": "solusdc", "amount": "50" },
  "txHash": "4Fq8pT2mVxT4nZb8sLw3YcR6uE1jHf5dKa9oQvN2x",
  "test": false
}

Paid in the asset you receive

A shop that receives TAO or a subnet token can also be paid in that same asset, and only that asset: TAO pays a TAO shop, SN7 pays an SN7 shop. The modal lists it first. The customer sends straight to your wallet. No exchange runs and we never hold the money.

The modal asks for an exact amount: the amount picked plus a random tail under 0.001. The exact amount is how we match the transfer to the payment.

Customer picksCustomer sendsamountsettled
5 TAO5.000137 TAO"5.000137"{"asset":"tao","amount":"5.000137"}
CaseResult
Exact amount, within 24 hours of startingpayment.delivered
Within 1% of the exact amount, and it can belong to one payment onlyYour dashboard shows Needs review. Accept it and payment.delivered is sent with the amount that arrived. Check the transaction first: we cannot know who sent it.
The same, with Accept automatically on in Settingspayment.delivered at once, with the amount that arrived
Any other amount, or laterIt lands in your wallet. No event. We cannot return it.
Sent from an exchangeMatched when the exchange sends the exact amount. The sender does not matter.
The same amount sent twiceThe first is matched. The second is not reported.
Sent in a batch, through a proxy, or as "transfer all"Not matched
Another asset sent by hand, e.g. alpha to a TAO shopNot matched. It lands in your wallet. No event.
You receive SOL, BTC, ETH or a tokenNot offered yet

A fixed-price shop checks amount >= price. Never check for an equal amount.

Verify the signature

Every POST carries x-allways-signature: t=<unix seconds>,v1=<hex>. v1 is the HMAC-SHA256 of <t>.<raw body> with your signing secret. Compute it over the raw bytes you received, before any JSON parsing. Reject a t more than 5 minutes from your clock.

The signing secret is on the Developers tab. Keep it on your server. Rotate replaces it at once.

This section has a Node sample and a Python sample. Select Show to open one.

Nodejs
const crypto = require('node:crypto');

// rawBody: the request body as a Buffer or string, exactly as received.
function verifyAllwaysWebhook(rawBody, header, secret, now = Date.now() / 1000) {
  const parts = Object.fromEntries(
    String(header).split(',').map((part) => part.split('=')),
  );
  const t = Number(parts.t);
  if (!Number.isInteger(t) || Math.abs(now - t) > 300) return false;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${t}.`)
    .update(rawBody)
    .digest('hex');
  const given = Buffer.from(String(parts.v1));
  return (
    given.length === expected.length &&
    crypto.timingSafeEqual(given, Buffer.from(expected))
  );
}
Pythonpython
import hashlib
import hmac
import time


# raw_body: the request body as bytes, exactly as received.
def verify_allways_webhook(raw_body, header, secret, now=None):
    parts = dict(part.split("=", 1) for part in header.split(",") if "=" in part)
    try:
        t = int(parts["t"])
    except (KeyError, ValueError):
        return False
    if abs((time.time() if now is None else now) - t) > 300:
        return False
    signed = f"{t}.".encode() + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

Retries

Answer with any 2xx within 5 seconds. Anything else is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours, then the delivery stops and shows as failed. Resend on the dashboard sends it again at any time.

  • The URL must be https and resolve to a public address. Redirects are not followed.
  • Each retry and each resend is signed again with a new t.
  • The same id can arrive more than once. Credit each id once.

Poll instead

Polling works without a webhook, and beside one. GET /v1/exchanges?updatedSince=<unix> returns every exchange updated at or after the cursor. It needs your API key.

Exchange fieldWebhook field
idid
referenceaccount
toasset
toAmountamount, when status is finished
statusfinished is payment.delivered; refunded is payment.refunded; failed with reason claim_stale, dest_unpayable or asset_unavailable is payment.failed

One exception: a refunded exchange whose refundAddress is your wallet was paid to you from collateral. Treat it as payment.delivered.

The cursor is inclusive, so the last exchange of one poll comes back in the next. Credit each id once. A payment made in the asset you receive is in the list too, with from equal to to. It can go from failed (deposit_lapsed) to finished when the transfer arrives late. The list also holds exchanges you created through the API. Test payments are never in it.

Nodejs
// Run on a timer. Persist `cursor` and the credited ids in your database.
async function pollPayments(cursor, alreadyCredited, credit) {
  const res = await fetch(`https://api-allways.venturalabs.ai/v1/exchanges?updatedSince=${cursor}`, {
    headers: { 'x-api-key': process.env.ALLWAYS_API_KEY },
  });
  if (!res.ok) return cursor;
  for (const exchange of await res.json()) {
    if (exchange.status === 'finished' && !(await alreadyCredited(exchange.id)))
      await credit(exchange.reference, exchange.toAmount, exchange.id);
    cursor = Math.max(cursor, exchange.updatedAt);
  }
  return cursor;
}

Vendor rules

  1. Verify the signature and the timestamp before any other step.
  2. Credit only on payment.delivered.
  3. Credit each id once.
  4. Credit from amount. Never from a value the browser shows.
  5. Ignore "test": true in production code paths.

Cost and limits

  • One credit per payment, taken from your balance when the customer starts it. It returns when the payment is refunded or fails. It is kept when the customer starts a payment and never sends.
  • With no credits left the modal shows a paused state. Your dashboard header shows the balance.
  • Up to 3 preset amounts, in the asset you receive. An amount under a pay asset's minimum cannot be paid with that asset; the dashboard warns when no asset can pay a preset.
  • Each customer IP can start 3 payments per minute.
  • A payment sent from an exchange account cannot be matched to the customer. The modal warns before the customer sends.
  • The dashboard lists the latest 100 webhook deliveries.

Paused

Customers see a paused state when:

CauseFix
Key missing, wrong, or rotatedCopy the snippet again
No checkout settings savedOpen Checkout widget and save Design your widget
No credits leftTop up

Where it works

  • Any page served over http or https. A file opened from disk cannot open the modal.
  • If your site sends a Content-Security-Policy, add https://allways.venturalabs.ai to script-src and frame-src.
  • Load checkout.js from https://allways.venturalabs.ai. Do not copy, bundle or proxy it: the modal accepts messages from its own origin only.
  • On a phone the modal opens as a bottom sheet.
  • Connect wallet runs in the modal. If a browser wallet does not answer there, the modal offers a small window on https://allways.venturalabs.ai, where the wallet steps run and the modal waits. The window closes when the customer pays. Send it myself always works.

Rotate the key

Rotate the key from Developers or from Exchange widget. The old key stops working at once in every checkout widget and every exchange widget.