Skip to main content

HTML forms

One way to add crypto payments to a site, an online store, or a bot. The customer clicks a button, and the payment page with the invoice opens.

The form is submitted only via POST to https://dash.bitsby.app/invoices/form and contains two fields:

FieldWhat it holds
database64url of the JSON with the invoice fields
signatureHMAC-SHA256 of the data string, 64 hex characters

A project can have at most 50 unpaid invoices created this way at any moment.

Form example

<form method="post" action="https://dash.bitsby.app/invoices/form" target="_blank">
<input type="hidden" name="data" value="eyJwcm9qZWN0SWQiOiJhMWIyYzNkNC01ZTZm...">
<input type="hidden" name="signature" value="f77d6da1be35bd2900e0bfed9f202b04...">
<button type="submit">Pay</button>
</form>

The store's server fills in both values when rendering the page. The secret never appears in the markup.

The target="_blank" attribute is optional: with it, the cart stays open in the original tab.

How the container is built

  1. Build a JSON object with the invoice fields.
  2. Encode it as base64url. This is the data string.
  3. Compute signature = HMAC-SHA256(data, formSecret).

The data string is signed as a whole, so the JSON key order, indentation, and Unicode escaping style do not matter. The receiver also accepts standard base64, with or without padding.

Do not rebuild data after signing: if even one byte changes, the signature must be computed again.

How to compute the signature, with ready-made examples in four languages, is covered in HTML form signature.

Keys inside data

KeyRequiredWhat it holds
projectIdyesProject ID from the settings in the dashboard, uuid
amountFiatyesAmount from 1 to 100000, at most two decimal places. As a string "10.50" or a number 10.5
currencyFiatyesFiat currency: USD, EUR, or RUB
timeToPayyesPayment deadline in hours: 0.5, 1, 3, 6, 12. As a string or a number
descriptionnoDescription for the customer, up to 1000 characters
serviceDatanoOrder identifier on the store side, up to 1000 characters. Not shown to the customer but visible in the form's source code—do not put anything sensitive here
outputnoerrors—show the details of validation errors

An optional key can be omitted from the JSON—that is the same as an empty string. Keys can go in any order; the receiver ignores unknown keys.

Values are JSON strings or numbers. Booleans, arrays, and nested objects do not count as field values and arrive as an empty string.

Example of the data contents:

{
"projectId": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"amountFiat": 10.5,
"currencyFiat": "USD",
"timeToPay": 1,
"description": "Order #7, delivery",
"serviceData": "order-7"
}

Here the amount and payment deadline are passed as numbers; strings are accepted too. The optional output is not set at all.

Amount format

Digits and a decimal point only, at most two decimal places: 10, 10.00, 1000.50. Surrounding spaces are trimmed.

Any other format is rejected—thousands separators, a comma instead of a point, exponential notation, a sign before the number, a currency symbol. There is no rounding: a third decimal place is not truncated, it causes a rejection.

Form secret

The secret lives in the project settings in the dashboard, in the Form secret field. The server issues it when the project is created. You cannot set your own value; the secret can only be reissued—for example, if it is compromised.

The secret must stay on the store's server. If it ends up in the storefront HTML, the signature becomes pointless.

The form secret and the webhook secret are different keys; do not mix them up.

Order identifier

Always fill in serviceData: put your own order identifier there. The value comes back in the payment notification—the store uses it to find the order.

A filled-in serviceData protects against duplicates. Resubmitting the form or double-clicking the button does not create a second invoice: the customer is sent to the existing unpaid one. The match is made on the order fields—project, amount, currency, description, and serviceData—not on the bytes of data.

The value must be unique per order. With the same value, a second customer lands on the first customer's unpaid invoice.

An empty serviceData removes this protection: there is nothing to recognize a repeat by, and every submit creates a new invoice. Duplicates use up the limit of 50 unpaid invoices, and when one is paid, the store has nothing to tie the payment to an order with.

Errors and debug mode

The form goes through two checks: first the signature and the container, then the field values.

What happenedWhat the service shows
The signature does not match, the container is unreadable, the project belongs to someone else or does not existA generic error, no reason given
The signature matches, but a field is filled in incorrectlyA generic error. With "output":"errors"—detailed errors
Both checks passedThe payment page with the invoice

The first check never reveals the reason, under any settings.

The "output":"errors" pair in the JSON enables details for the second check: the service names the field, lists the allowed values, and reports when the ceiling of 50 unpaid invoices is reached. The key lives inside the signed container, so it cannot be injected from outside.

While setting up the form, put "output":"errors" in the JSON; once set up, remove the key and rebuild the container.

If the signature itself does not match, compare your data and signature against the reference set in HTML form signature.

What's next

An invoice created by the form is processed the same way as one from the API or the dashboard:

  • Payment confirmation arrives at the Webhook URL. Verify the notification signature with the webhook secret and fulfill orders only based on it.
  • Returning the customer to your site is configured via the Successful URL and Unsuccessful URL—see the “Returning the customer to the store site” section on the Invoice lifecycle page. Landing on these URLs does not confirm payment.
  • Invoice statuses and the payment search window are described in Invoice lifecycle.
  • The customer transferred the wrong amount—see Payment matching and amount mismatches.