Skip to content
Hypamoon
API docs

Start for free. Sign up without a card and you'll get a test project and a test key right away. Once your company (or, for a personal account, you) is verified, you can create a live key in the portal.

Payments: sell on your own site

Everything on this page works; where a part has a limit, it is marked. Your chat can always tell you where your project stands: get_payments_status returns availability (live) and mode (live when the project takes real payments, test when every payment is a test payment and no money moves). The same two words are used everywhere: in the tools, in the portal and on this page.

How the money moves

You are the seller. You connect your own payment account in the Hypamoon portal (Payments), and the money of every sale goes straight to that account. Hypamoon never holds it. Hypamoon takes an application fee per successful payment, only after the owner of the project has agreed to it in the portal. A test project always pays in test mode.

You are also responsible for what a seller has to publish: your terms, your privacy notice, your returns policy and, if you sell to consumers in the EU, VAT. Where available, a paid order gets an invoice in your name, never in Hypamoon's (see VAT and invoices); your order data is yours to export.

Products

A product is a document in the product collection of the project. Create that collection once:

  • with MCP: create_collection with "role": "products" (or, on connections without the shop settings, create_product_collection);
  • with REST: POST /api/v1/collections with {"name": "products", "role": "products"} (or POST /api/v1/payments/product-collection).

Then add products with insert_document. The fields that matter for the checkout:

field meaning
name What the buyer sees, 1 to 120 characters.
price_cents A whole number in the smallest unit of the currency, like 1995 for 19.95. VAT included, unless the shop setting prices_include_vat is false.
currency Optional, ISO 4217. Left out: the shop currency (EUR by default). One checkout is one currency. See Currencies.
stock Optional whole number. Left out: unlimited. It goes down when an order is paid.
active Optional. false takes the product off sale.
line_fields Optional, at most 3 questions per order line, like an engraving or a name on a cake. Same shape as checkout_fields below.
tax_class Optional, where available: standard (default), reduced, zero or exempt. Which VAT rate of the shop's rate table the product gets (see VAT and invoices). A variant may have its own tax_class.
kind Optional: physical (default), digital, service or gift_card. With VAT per EU country, goods and digital products follow the buyer's country; a service stays in the seller's country.

Other fields (description, image_url, sku, anything you like) may be added; the checkout ignores them. The product collection can be read by a site but never written by one: a visitor can never set a price.

Currencies

The currency is your choice, per product. Set currency on a product, or a default for the whole shop with the shop setting currency; a product without either is EUR. Hypamoon never converts a price: if you sell in EUR and GBP, you set both prices yourself, as two products.

You can sell in EUR, USD, GBP, CAD, AUD, CHF, SEK, NOK, DKK and PLN. Currencies without cents (like JPY) are not available. What a buyer can pay with depends on the currency and on your payment provider:

payment provider for Europe second payment provider (outside Europe)
One payment in EUR Every method you turned on, like iDEAL, Bancontact, cards, Apple Pay and PayPal. Every method you turned on.
One payment in another currency Cards, Apple Pay and PayPal. iDEAL and Bancontact are EUR only. Cards and Apple Pay, plus the local methods your account offers in that currency.
A subscription EUR only. Every currency above.

Everything after the sale stays in the currency of the order: the order, the invoice and credit notes, a gift card bought with it and its balance, and every refund. A gift card only pays for an order in its own currency.

The shop setting payment_methods (optional: card, apple_pay, paypal, ideal, bancontact) limits what the checkout offers. When none of the chosen methods can take the order's currency, the checkout refuses the order with currency_not_available, and configure_shop refuses the setting, with the way out in the message (add a method that can, leave payment_methods out, or price in EUR).

Selling in another currency than EUR needs fee terms that list the fee per currency. Terms agreed to before 14 October 2026 cover EUR only; the owner agrees to the current terms in the portal (Payments). get_payments_status shows currencies: what the project can sell in once (one_time) and as a subscription (subscriptions), the methods outside EUR, and whether the fee terms cover other currencies.

Variants (where available)

A product that comes in sizes or colours stays one document, with options and variants:

JSON
{
  "name": "T-shirt",
  "price_cents": 2500,
  "options": [
    { "name": "Size", "values": ["S", "M", "XL"] },
    { "name": "Colour", "values": ["Blue", "Black"] }
  ],
  "variants": [
    { "id": "s-blue", "options": { "Size": "S", "Colour": "Blue" }, "stock": 4, "sku": "TS-S-BL" },
    { "id": "xl-black", "options": { "Size": "XL", "Colour": "Black" }, "price_cents": 2700, "stock": 2, "image_url": "https://example.com/xl-black.jpg" }
  ]
}
field meaning
options At most 3, each with a name and 1 to 50 values.
variants At most 100. Each has an id (letters, digits, _ and -, unique in the product) and a value for every option.
variants[].price_cents Optional. Left out: the product's price_cents.
variants[].stock Optional. Left out: the product's stock counts for this variant (a shared stock), and without that it is unlimited.
variants[].active Optional. false takes only this variant off sale.
variants[].sku, variants[].image_url Optional.

A product with variants sells only a variant: every line names a variant_id. The stock goes down on the variant when the order is paid, and back up on a full refund that was never shipped. When a product document does not fit these rules, insert_document says which field is wrong (like data.variants.2.options.Size). A product without variants works exactly as before.

Importing from WooCommerce turns a variable product and its variations into one product with variants. Products imported earlier as separate variations stay as they are.

The buy button

get_checkout_snippet returns a button and the script tag:

html
<button type="button" data-hypamoon-checkout data-hypamoon-token="hm_pub_..." data-hypamoon-product="<product id>">Buy now</button>
<p data-hypamoon-checkout-status role="status"></p>
<script src="https://api.hypamoon.com/checkout.js" defer></script>

Options on the button: data-hypamoon-items (a JSON list of {"product_id", "quantity", "fields"} for several lines), data-hypamoon-quantity, data-hypamoon-shipping="true" (ask for an address), data-hypamoon-return (a path on the site to come back to), and data-hypamoon-terms and data-hypamoon-privacy (https links, used when the shop settings have none). The button uses the project's public token; the site's origin must be in its allowed_origins.

When the visitor clicks, a small window asks for an email address (and an address with shipping) and runs a bot check. Before the pay button it shows the total, the name of the seller and the links to the seller's terms and privacy notice; without a total the pay button stays off. Then the visitor goes to the payment page and comes back with ?hypamoon_order=<id> and a signed ?hypamoon_ref= (see "After paying" below).

For a product with variants, add data-hypamoon-variant="<variant id>" (or variant_id in each line of data-hypamoon-items). For a shop with more than one product, the cart below is usually the better fit.

When the shop cannot take payments right now (the quote says checkout_available: false, for example because the owner has to connect payments again), the window shows no pay button but a short line for the buyer with the shop's contact link, and a warning in the browser console for you. In test mode (test_mode: true) it shows a "Test mode" label next to the title and "No real money moves." under the pay button.

After paying: the confirmation

You do not have to build a thank-you page. The payment provider and the test payment page both send the buyer back to the page of the button (or data-hypamoon-return), whatever happened. checkout.js (1.5.0 and later) then shows a window in the same style as the checkout window, with the same --hm-* variables:

What happened The window
Paid "Thank you, your order is confirmed", the order number, what was bought, the total and "A confirmation is on its way to ". For a gift card: that it is attached to that email (and goes to the recipient, or on which day), with Download gift card (PDF) and Print.
Not paid yet (the payment provider has not told us yet, or the buyer came back without finishing) "We are waiting for your payment". It checks again by itself (every 2 seconds, then every 5, for 5 minutes), with Continue to payment to the payment page.
Failed, canceled or expired "The payment did not go through", with Try again (opens the checkout with the same products, or the cart).
Refunded "This order was refunded".

The status line from the snippet (data-hypamoon-checkout-status) gets the same short sentence. Escape and the close button close the window; focus goes back to the status line.

How it works: the return URL carries the order id and a signed reference (hypamoon_ref, an HMAC of the order id with a server key). checkout.js reads the order with GET https://api.hypamoon.com/api/v1/public/orders/<order id>?ref=<reference> and then takes the reference out of the address bar (it keeps it in sessionStorage for a reload in the same tab). Without the right reference that route answers 404, like for an order that does not exist; order ids or numbers alone open nothing. It returns only what the buyer entered or bought: status, order number, the lines, the total, the buyer's email, test or not, and the gift cards (value, whether they go to a recipient, never who, and never the code). The confirmation email links to the same page with the reference ("View your order"), so a buyer who closed the tab can come back.

The gift card PDF (/api/v1/public/orders/<order id>/gift-cards/<n>?ref=, &download=1 to download) is made on request and not stored. It works for 30 days after the payment; after that the PDF in the email and the balance page still have the card. The code itself is never shown in the window: the window runs on your page, where other scripts (analytics, chat, session replay) can read what it shows, and a code is money.

Your own texts or your own confirmation:

  • data-hypamoon-paid-title, data-hypamoon-pending-title and data-hypamoon-failed-title on the button or the status line replace the titles. The value of data-hypamoon-checkout-status replaces the paid sentence in the status line.
  • data-hypamoon-confirmation="false" on the button, the status line or the script tag: no window. The status line still updates, and every check fires hypamoon:order on window with { status, order_id, order_number, test, total_cents, currency, items } (status is paid, pending, failed or refunded), so you can show your own.

Orders from before 1.5.0 have no reference in their return URL: then only the status line shows its sentence, as before.

checkout.js and cart.js are served with Cache-Control: max-age=300, stale-while-revalidate=3600: a new version reaches visitors within minutes, without a version in the script URL.

The look of the checkout window

Hypamoon is the plumbing; the site picks the style. The window lives in a shadow root, so the site's own CSS (like button {} or input {}) does not leak into it. It starts calm and neutral (rounded corners, a soft shadow, light and dark through prefers-color-scheme) and then takes over the site's look in three layers, the first one wins:

  1. CSS variables set by the site, on :root or on the button itself ([data-hypamoon-checkout]). The window reads them when it opens.
  2. The appearance of the shop from GET /api/v1/public/appearance (the same as the cart).
  3. The neutral style.
Variable What it sets
--hm-accent The pay button and checkboxes
--hm-accent-text Text on the pay button (default: black or white, whichever reads best on --hm-accent)
--hm-font The font of the window
--hm-font-heading The font of the title
--hm-radius Corner radius of fields and buttons (the window itself is a bit rounder)
--hm-bg Background of the window
--hm-surface Fields and the summary box
--hm-text Text
--hm-muted Labels and small print
--hm-line Thin dividers
--hm-border Borders of fields and secondary buttons
--hm-focus The focus ring
--hm-error Error messages

Without --hm-font the window uses the font of the page (the font-family of body), and without --hm-font-heading the font of the first h1 or h2 on the page. The older --accent, --primary and --brand on :root still work as a fallback for --hm-accent. If you set --hm-bg but not the text colours, the window picks neutral light or dark colours that go with your background.

For dark mode there are no separate names: set the same variables again inside a prefers-color-scheme block.

css
:root {
  --hm-font: "Hanken Grotesk", system-ui, sans-serif;
  --hm-font-heading: "Source Serif 4", Georgia, serif;
  --hm-accent: #9a4f27;
  --hm-accent-text: #ffffff;
  --hm-bg: #f6f1e9;
  --hm-surface: #ede4d6;
  --hm-radius: 4px;
}
@media (prefers-color-scheme: dark) {
  :root {
    --hm-accent: #de9d6e;
    --hm-accent-text: #1c120b;
    --hm-bg: #17120e;
    --hm-surface: #221a14;
  }
}

The window is a modal <dialog> with its title as label: Tab and Shift+Tab stay inside it, Escape and the close button close it, and focus goes back to the button that opened it. Fields have their label above them and are at least 44 px high; on a phone the window fills almost the whole screen and scrolls inside.

The cart (where available)

get_cart_snippet gives a drop-in shopping cart: one script line for every page, a cart button for the header, and an add-to-cart button per product.

html
<script src="https://api.hypamoon.com/cart.js" data-token="hm_pub_..." defer></script>
<hypamoon-cart></hypamoon-cart>
<hypamoon-add-to-cart product="<product id>">Add to cart</hypamoon-add-to-cart>
<hypamoon-add-to-cart product="<product id>" variant="xl-black">Add to cart</hypamoon-add-to-cart>
  • The cart button shows a counter and opens a drawer where the buyer changes quantities, removes lines and picks a variant. Without <hypamoon-cart> on the page a floating cart button appears (turn that off with data-floating="false" on the script tag).
  • An add-to-cart button for a product with variants and without variant shows a variant picker next to it; sold-out variants cannot be picked. Add open="false" to keep the drawer closed after adding.
  • The cart keeps only product ids, variants and quantities, in the visitor's browser per public token. Every amount, name and stock message comes from the quote below, never from the page.
  • Checkout opens the same checkout window as the buy button. Options on the script tag: data-shipping="true", data-return="/thanks", data-terms, data-privacy and data-lang (en or nl; otherwise the page's <html lang>).
  • Back from the payment page, the cart (1.1.0 and later) loads checkout.js, which shows the confirmation (see "After paying" above). The cart empties only when the order is paid; after a failed payment the lines stay, and Try again opens the checkout from the cart.
  • It takes the site's colours (light and dark), fonts, corner radius and language from GET /api/v1/public/appearance?token=.... Override anything with CSS variables on the element, like hypamoon-cart { --hm-accent: #0a7b55; --hm-radius: 4px } (also --hm-bg, --hm-surface, --hm-text, --hm-muted, --hm-line, --hm-border, --hm-accent-text, --hm-focus, --hm-error, --hm-font, --hm-font-heading), or with ::part(button), ::part(count), ::part(drawer) and ::part(checkout).
  • Keyboard and screen readers: every control is a real button, select or input with a label, the drawer is a modal dialog (Escape closes it and focus goes back to the cart button), and the counter is announced.

Your own cart

The same script gives window.Hypamoon.cart for a cart in your own design:

Node.js
Hypamoon.cart.add(productId, { variant: "m-blue", quantity: 1, fields: { engraving: "Anna" } });
Hypamoon.cart.remove(0);              // by index, or { product_id, variant_id }
Hypamoon.cart.setQuantity(0, 3);
Hypamoon.cart.setVariant(0, "s-blue");
Hypamoon.cart.get();                  // [{ product_id, variant_id, quantity, fields }]
Hypamoon.cart.quote();                // a Promise with the quote below
Hypamoon.cart.open();                 // the drawer; open(items) replaces the cart first
Hypamoon.cart.checkout();             // straight to the checkout window
window.addEventListener("hypamoon:cart", (e) => console.log(e.detail.type, e.detail.count, e.detail.quote));

Without the script, call the quote yourself and start the checkout with the same items: the amounts are the same either way.

The quote: one place where amounts are calculated

POST /api/v1/public/cart/quote?token=hm_pub_...&lang=en calculates an order without starting it. The checkout uses the same calculation to create the payment, so a cart you build yourself always shows the amount the buyer will pay. Public, with the public token and its allowed origins, like a form.

Request:

JSON
{
  "items": [{ "product_id": "<id>", "quantity": 2, "fields": { "engraving": "Anna" } }],
  "fields": { "delivery_date": "2026-12-24" },
  "country": "NL",
  "code": "WELCOME10"
}

Answer (amounts in the smallest unit, like price_cents):

JSON
{
  "ok": true,
  "currency": "EUR",
  "prices_include_vat": true,
  "items": [{ "product_id": "<id>", "name": "Mug", "unit_price_cents": 1495, "quantity": 2, "subtotal_cents": 2990, "discount_cents": 0 }],
  "subtotal_cents": 2990,
  "discount_cents": 0,
  "discount_code": null,
  "shipping": { "cents": 0, "method": null, "options": [] },
  "tax": { "mode": "not_calculated", "included": true, "total_cents": null, "lines": [], "note": null },
  "gift_card_cents": 0,
  "total_cents": 2990,
  "to_pay_cents": 2990,
  "total": "29.90",
  "total_display": "€29.90",
  "seller": { "name": "Example Shop B.V." },
  "links": { "terms": "https://example.com/terms", "privacy": "https://example.com/privacy", "returns": null, "contact": null },
  "checkout_fields": [{ "key": "delivery_date", "label": "Delivery date", "type": "date", "required": true, "max_length": 500 }],
  "problems": []
}

problems lists what stops a payment, each with code, path and message: not_available, sold_out, not_enough_stock, mixed_currency, currency_not_available, invalid_fields and code_invalid. A quote with problems is a 200 with ok: false; show the messages to the buyer.

With variants (where available), each line in items may have a variant_id, and each line in the answer also has variant_id, variant_name (like "M / Blue"), options (like {"Size": "M"}), image_url, needs_variant and, for a product with variants, choices: the product's options and its variants that are on sale, each with id, label, options, price_cents and available. A line without a variant for such a product comes back with needs_variant: true and the problem variant_required; an unknown or switched-off variant gives variant_not_available. code_field says whether the shop takes a code. Stock that another buyer is paying for right now counts as gone (see the checkout).

With VAT set up in the shop (where available, see VAT and invoices), tax has the calculation: mode (none, domestic, eu_oss or stripe_tax), included, total_cents, lines per rate with rate, country, treatment (standard, reverse_charge, export, exempt, outside_scope, none), base_cents and tax_cents, the country of the rate, vat_number (the buyer's number with its status and whether VAT was reverse_charged), notices for the buyer, and a note for the receipt. Send vat_number in the request for a business buyer, and country as soon as the buyer's country is known. Without a VAT setting, tax.mode stays not_calculated and the prices are what you set.

Not here yet: discount codes and shipping rules. Until then discount_cents and shipping.cents are 0. Include shipping in your prices for now.

Downloads (where available)

Sell a file (an e-book, a pattern, music, a font, a zip) as a download. The file is private: it is never on a site, and buyers only get links that expire.

  1. create_download_upload_urls with name, sha256 and bytes per file. You get an upload_url per file: PUT the exact bytes there within 15 minutes. Types: pdf, zip, epub, mp3, m4a, wav, flac, mp4, png, jpg, webp, gif, svg, txt, csv, json, woff2, ttf and otf. A file can be as large as a video on your plan.
  2. Put the returned file objects on the product: {"name": "Recipes e-book", "price_cents": 1200, "delivery": "download", "files": [...]}. At most 10 files per product; a variant can have its own delivery and files (a CD and an MP3 of the same album). "kind": "digital" means the same as "delivery": "download".

At checkout the buyer ticks that delivery starts right away and that they lose their right of withdrawal (EU law for digital content); without the tick the checkout is refused with an error on waive_withdrawal. An order with only downloads asks no shipping address. After payment the order confirmation has one link to the buyer's download page. Each file works a number of days and a number of downloads (shop setting downloads, default 7 days and 5 times; or download: {"expires_days": 30, "max_downloads": 3} on the product). Opening the page counts nothing; only the download button does. When the links have expired or are used up, the buyer asks for new ones on the same page (they go to the email of the order) or on their orders page. A full refund closes the links. A download product without files cannot be bought; check_business_readiness lists files that are missing.

My orders for buyers (where available)

Every site of the project has /_hypamoon/orders: buyers enter the email of their order and get a sign-in link (30 minutes, once). They see their paid orders with the status, tracking, invoices, gift cards they bought with the balance left, their downloads and a button for new links, and they can download all their data or have it deleted. Orders they paid for stay with you for your accounts, with their name and address but without their email. On a site with an app with sign-in, a signed-in member is a buyer with the same email, without a second sign-in. Link to it from your site, like "My orders". The checkout fills in the name, email and last address of a buyer who is signed in on that site. Turn it off with the shop setting buyer_account.

Product feed (where available)

A public feed of your active products at a fixed address, shown by get_shop as feed:

  • https://api.hypamoon.com/api/v1/public/shops/{project_id}/products.xml for Google Merchant Center (add it there as a scheduled fetch);
  • .../products.json, the same items as simple JSON.

Each variant is an item with item_group_id set to the product, with colour, size and material from its options. Price from the server (a sale price when compare_at_cents is higher), availability from the stock, the image of the variant or the product, and the link from the product's url or the pattern in the shop setting feed.product_url (like https://example.com/products/{slug}, with {slug}, {id} or {sku}). A product without a link is left out; check_business_readiness says which. Products with "feed": false, access products, gift cards and services are never in it. Updated every five minutes. Turn it off with feed.enabled.

The checkout

POST /api/v1/public/checkout?token=hm_pub_... is what checkout.js calls. It takes the same items and fields as the quote, plus customer (email, optional name; where available also company, vat_number and country, the country for VAT when there is no address), an optional shipping_address, an optional billing_address (the address on the invoice, where available), an optional return_path, a turnstile_token from the bot check, waive_withdrawal (true, required when the order has a download) and an idempotency_key (the same key twice gives one order). It answers with order_id and checkout_url; send the visitor there. Limits: 20 checkouts per network per hour, 300 per project per day.

Where the shop settings are available, starting a checkout holds the stock of its lines for 30 minutes, or until the payment fails, expires or is canceled. Two buyers can never pay for the last item: the second one gets a 422 with the detail Not enough stock. before any payment starts.

Orders

list_orders and get_order (REST: GET /api/v1/orders and GET /api/v1/orders/{id}). Statuses: open, paid, shipped, canceled, refunded, and charged_back or disputed when the buyer's bank is involved. Every order is also copied into a private collection orders that can never be public; the copy is for reading.

Where the shop settings are available, an order also has:

field meaning
number, order_number The order number from the project's own series, like QS-1001, next to the id.
fields The answers to the shop's checkout_fields; items[].fields has the answers per line.
amounts The breakdown: subtotal, discount, shipping, tax, tax_included, gift_card, total.
amount_refunded, refunds What has been refunded so far, also partly, with each refund (id, amount, at, via api or dashboard).
tracking number and url, from update_order_status with shipped.

Mark an order shipped with update_order_status (REST: PATCH /api/v1/orders/{id} with {"status": "shipped", "tracking_number": "...", "tracking_url": "https://..."}).

Emails to the buyer

  • Order confirmation, always, once, when the order is paid. In the language and colours of the site, with the order number, the lines, the total, the buyer's answers, the seller's name, address and contact, the links to terms and privacy and, for physical products, the 14-day right of withdrawal with the model form. In the EU the buyer must get this confirmation by email; it cannot be turned off. With downloads it also has the link to the download page and the text the buyer ticked.
  • Shipped, with the tracking link, when you mark an order shipped (shop setting emails.shipped).
  • Refunded, with the amount, after every refund (shop setting emails.refunded).

The emails come from a Hypamoon address with the shop's name as the sender, and replies go to the shop's contact_email (by default the project's billing email).

Refunds

refund_order (REST: POST /api/v1/orders/{id}/refund) refunds a paid or shipped order from your payment account, after an explicit yes from the user (confirm: true). Calling it twice refunds once.

  • Without amount_cents: everything that is left. The order becomes refunded, and the stock that went down for it goes back up, once, unless the order was shipped. The answer has stock_restore with what happened.
  • With amount_cents (where the shop settings are available): only that part. The order stays paid or shipped, amount_refunded goes up, the stock stays. You cannot refund more than is left.

A refund you make in the dashboard of your payment account, in full or in part, is picked up the same way: the order shows it and the buyer gets the refund email.

Shop settings (where available)

get_shop and configure_shop (REST: GET and PATCH /api/v1/shop). One object; send only what changes, null clears a field. get_shop also shows the seller as buyers see them, from set_company_details, with what is missing.

setting meaning
currency The default for products without a currency. Default EUR. See Currencies.
payment_methods Optional: which of card, apple_pay, paypal, ideal, bancontact the checkout offers. Left out: everything you turned on that works in the order's currency.
prices_include_vat true by default, as consumers in the EU must see prices.
shop_name The sender name of buyer emails. Default: the site's name.
contact_email, contact_phone Where buyers reach you; the reply-to of every buyer email.
links terms, privacy, returns, contact: https links shown in the checkout and in every confirmation. You publish these pages yourself.
order_number prefix (like QS-) and start (default 1001).
checkout_fields At most 5 questions to the buyer: key, label (text, or {"en": "...", "nl": "..."}), type (text, textarea, select, checkbox, date), required, options for a select, max_length. A field you did not declare is refused with an error that names it.
withdrawal_right true by default. false only if nothing in the shop has the right of withdrawal (made to order, perishable).
stock deactivate_when_sold_out (set active to false at stock 0) and low_threshold (send stock.low).
emails shipped, refunded (both on by default) and a footer line.
gift_cards desk_for_viewers (let viewers redeem at the desk), redeem_url (your own balance page; the QR links there with the code after #) and terms (a short line printed on every card).
tax How VAT works in the shop: mode, country, none_reason, rates, country_rates, rates_confirmed, oss_pricing, reverse_charge, stripe_tax. See VAT and invoices.
invoices enabled (default true), prefix (default INV-), attach (the PDF goes with the order confirmation, default true) and note (a line at the bottom of every invoice).
downloads expires_days (default 7) and max_downloads per file (default 5) for download links.
buyer_account true by default: buyers see their orders at /_hypamoon/orders.
feed enabled (default true), product_url (a link pattern with {slug}, {id} or {sku}) and brand.

Gift cards (where available)

Sell gift cards on your site, issue them at the desk, and redeem them at the desk or online. Every card comes as a PDF in the look of your site that buyers can print or forward.

A gift card is a product. Add a document to the product collection with kind: "gift_card" and a gift_card block:

JSON
{
  "name": "Gift card", "kind": "gift_card", "price_cents": 5000,
  "gift_card": {
    "mode": "amount",
    "amounts": [2500, 5000, 7500],
    "open_amount": { "min_cents": 1000, "max_cents": 25000 },
    "validity_months": 24,
    "redeem": "both",
    "vat": "multi"
  }
}
  • mode: amount (a value in money) or service with service, like "One haircut"; then price_cents is what the card is worth.

  • amounts and open_amount: the buyer picks a fixed amount or any amount in the range. Without them the card is worth price_cents.

  • validity_months: at least and by default 24. In the Netherlands a gift card for consumers must be valid for at least two years.

  • redeem: in_store, online or both.

  • vat: multi (default) when the card can be used for things with different VAT rates; VAT is then due when it is used. single with vat_rate when it is for one thing with a known rate, like a haircut at 9%; VAT is then due when it is sold. Ask your accountant if you are not sure.

  • pdf (optional): the look of the PDF card. Leave it out and the card follows the site it was bought on: its name, accent colour (--hm-accent, the same variable the checkout reads), its own fonts and its icon next to the name. To change that:

    • accent: a colour like "#9a4f27" for the card. When no text is readable on it, the default stays.
    • logo: a path to an image on that same site, like "/logo.png" (png, jpg, webp, gif, or an SVG with only shapes in it), at most 512 kB. It replaces the name on the card. "none" prints only the name.
    • font: site (default), sans, serif or mono.

    The PDF never loads anything from another host: only files from the site itself.

The checkout asks the buyer, per card: amount_cents (when there are amounts or an open amount), recipient_name, recipient_email, message and send_on (a date). Send them as fields on the line.

After payment every card gets its own 16-character code (like K7QM-3XRD-9PTA-WC2H). The buyer gets an email with the card as a PDF; with recipient_email the recipient gets it too, right away or on send_on. The code is never stored in readable form: only as a fingerprint and encrypted.

Redeeming online. The checkout has one field for a discount or gift card code (code on POST /api/v1/public/checkout and the quote). A gift card pays after VAT: it comes off the total, not off the price. A card cannot buy another card, and a card for in_store only does not work online. When the card covers everything, the order is paid at once without a payment page. When it covers part, the buyer pays the rest. If the rest would be below the minimum of the payment provider, the card is used for a little less.

At the desk. Open Gift cards in the portal (/gift-cards), type the code or scan the QR with the phone camera, see the balance and take off part or all of it. The QR on the card opens a balance page with a download of the PDF, and a link to the desk for staff. Every change is in the card's history, with who did it.

From the API or your chat.

tool REST what
issue_gift_card POST /api/v1/gift-cards A card without a payment: sold at the desk, or as an apology. Needs amount_cents and a reason; with email it is sent as a PDF (or on send_on).
check_gift_card POST /api/v1/gift-cards/check Balance, valid until, status and history, by code or id.
redeem_gift_card POST /api/v1/gift-cards/redeem Take amount_cents (or "all") off. Send an idempotency_key: the same key never takes money off twice. The balance never goes below zero.
list_gift_cards GET /api/v1/gift-cards Newest first, with status, limit and before.
void_gift_card POST /api/v1/gift-cards/{id}/void Cancel a card with a reason. Cannot be undone.
POST /api/v1/gift-cards/{id}/adjust Correct the balance (plus or minus) with a reason and an idempotency_key.
GET /api/v1/gift-cards/{id} One card with its history.
GET /api/v1/gift-cards/{id}/pdf The card as a PDF (?download=1 to download).

Only an owner or builder can issue, redeem, cancel and correct. A viewer can redeem at the desk when you turn on gift_cards.desk_for_viewers. Editors never touch gift cards.

Refunds. A full refund of an order puts the gift card part back on the card, and cancels cards that were bought in that order and not used yet.

Expiry. A card past its date expires by itself; its history says so.

VAT and invoices (where available)

You are the seller, and you stay responsible for your VAT. Hypamoon calculates the VAT with the rates you confirm and puts it on an invoice in your name. The rate table we provide is a starting point to check, not tax advice. Check the rates for your products with your accountant or the tax authority before you sell. Hypamoon does not file any VAT return.

Three modes

Set tax.mode with configure_shop:

mode for what happens
none A small business scheme (in the Netherlands the KOR), or a seller outside VAT. No VAT on anything. The confirmation and the receipt say why (none_reason: small_business, not_registered or exempt).
domestic Most small shops: one country. The rates of your own country (tax.country, by default the country of your address) for every buyer. Below the EU threshold for distance sales (10,000 euro a year to consumers in other EU countries together) this is also right for buyers elsewhere in the EU.
eu_oss Above that threshold, registered for the One Stop Shop. Goods and digital products to a consumer in another EU country carry the VAT of that country. Services (a haircut, a lesson) stay at your own rate: they happen where you are. With prices including VAT, oss_pricing same_gross (default) keeps the price the same everywhere; same_net keeps the amount without VAT the same, so the price follows the rate.

Without a mode, nothing changes: VAT is not calculated and prices are what you set.

Prices including or excluding VAT. prices_include_vat (default true): the VAT is taken out of the price. With false (a shop for businesses), the VAT comes on top.

Rates per product. Every product (or variant) has tax_class: standard, reduced, zero or exempt. The rate comes from the table for the country, or from your own tax.rates ({"standard": 21, "reduced": 9}) and, with eu_oss, tax.country_rates ({"FR": {"reduced": 5.5}}). Where a country has more than one reduced rate, the table uses the highest; set your own when your product has another. get_shop with include_vat_rates returns the table with its date and source. When you have checked your rates, configure_shop with {"tax": {"rates_confirmed": true}}; when the table changes, check_business_readiness asks you to confirm again.

The order of the calculation. A discount comes off the price before VAT (it lowers the taxable amount); shipping is added and follows the rates of the lines it ships; then VAT; a gift card pays from the total after VAT, as a means of payment. A gift card for several purposes (gift_card.vat multi) has no VAT when it is sold, only when it is used; one for a single purpose carries its rate when it is sold.

Exports. Goods and digital products to a buyer outside the EU have no EU VAT (treatment: "export"); with prices including VAT, the VAT comes off.

Business buyers in the EU

The buy button and the cart do this for you: with VAT set up, the checkout shows the VAT amount and a box "I am buying for a business" with company name and VAT number (and, with eu_oss and no shipping address, the buyer's country). In your own cart, the checkout takes customer.vat_number (and the quote vat_number). We check it in the EU register VIES. Valid, from another EU country than yours, tax.reverse_charge on (the default) and your own VAT number known: no VAT, the price without VAT, and the invoice says "VAT reverse charged". The VIES consultation number is kept with the order as your proof. Not valid, or VIES not reachable right now: VAT is charged as for anyone else, the payment goes on, and the quote has a notice for the buyer in tax.notices.

When your payment provider calculates the VAT

Outside Europe, payments run on a second payment provider (see How the money moves). That provider can calculate the VAT itself on your own account. With tax.stripe_tax: true on such a project, it calculates the VAT in its checkout instead of our table, asks for the billing address, collects business VAT numbers, and creates the invoice on your own account there, so Hypamoon does not make one. You set up your tax registrations with that provider, and it charges for this. Prices must include VAT, so the amount the buyer pays never changes after the order is made.

The payment provider for Europe does not calculate VAT. There, our own calculation and rate table always apply, and Hypamoon makes the invoice. The amount sent to the provider is the total with VAT.

The invoice

Once tax.mode is set and your details are complete (set_company_details: name, address, company number, and your VAT number unless the mode is none), every paid order gets an invoice in your name:

  • its own number from your series, like INV-0001, running on without gaps (a test order gets TEST-INV-0001 from a separate series);
  • the date, your name, address, company number and VAT number, the buyer's name and address (and company and VAT number for a business), each line with quantity, unit price without VAT, rate and amount, the taxable amount and VAT per rate, the total, what a gift card paid, and the notes the law asks for (reverse charge, OSS, no VAT);
  • in the colours, letter and logo of your site;
  • attached to the order confirmation as a PDF (setting invoices.attach), and always as a link the buyer can open later.

An invoice never changes. A refund, in full or in part, adds a credit note with the next number, the refunded amount spread over the rates of the invoice, and a reference to the invoice. It goes with the refund email.

Hypamoon is not on the invoice: you are the seller.

tool REST what it does
get_invoice GET /api/v1/invoices?order_id=..., GET /api/v1/invoices/{id}, GET /api/v1/invoices/{id}/pdf The invoice of an order and its credit notes, with pdf_url and buyer_url.
create_invoice POST /api/v1/invoices with {"order_id": "..."} Makes the invoice of a paid order that has none, for example one paid before your details were complete.
export_orders GET /api/v1/shop/export?format=...&from=...&to=... CSV for your bookkeeping.

The invoices are also under Payments in the portal, next to each order.

Export for your bookkeeping

export_orders gives a CSV over a period (from included, to not; by default the current quarter):

  • invoices: one row per invoice and credit note;
  • vat: sales per VAT rate, country and treatment, credit notes subtracted;
  • oss: only the sales with the VAT of another EU country, per country and rate, for your quarterly OSS return.

Amounts use a dot (12.50). A link with an accounting package comes later through webhooks; until then, the export is the way.

Webhooks

Subscribe with create_webhook (REST: POST /api/v1/webhooks), signed like every other webhook.

event when data
order.paid A buyer paid. order
order.shipped You marked the order shipped. Where available. order
order.refunded Money went back, in full or in part, through the API or the dashboard. One event per refund. Where available. order
order.canceled An open order was canceled or its payment expired. Where available. order
stock.low A paid order brought a product to the shop's low_threshold or below. Where available. product
gift_card.issued A card was issued, after a payment or with issue_gift_card. Where available. gift_card (no code, no email address)
gift_card.redeemed Money was taken off a card, at the desk, online or through the API. gift_card, amount_cents, channel, order_id
gift_card.expired A card passed its date with a balance left. gift_card, expired_cents

Collections with a role

create_collection with role makes a collection the shop uses: products (the product collection) or orders (the private copy of the orders, made by the first checkout if you do not make it yourself). discounts comes later. Gift cards are not a collection: use the gift card tools above.

REST at a glance

method path scope
GET /api/v1/payments project:read
POST /api/v1/payments/product-collection data:write
GET /api/v1/shop data:read
PATCH /api/v1/shop project:write
GET /api/v1/orders data:read
GET /api/v1/orders/{id} data:read
PATCH /api/v1/orders/{id} data:write
POST /api/v1/orders/{id}/refund project:write
GET /api/v1/public/shops/{project_id}/products.xml and .json none (public)
POST /api/v1/public/cart/quote public token
POST /api/v1/public/checkout public token
GET /api/v1/public/appearance public token
GET /api/v1/gift-cards data:read
POST /api/v1/gift-cards data:write
POST /api/v1/gift-cards/check data:read
POST /api/v1/gift-cards/redeem data:write
GET /api/v1/gift-cards/{id} data:read
GET /api/v1/gift-cards/{id}/pdf data:read
POST /api/v1/gift-cards/{id}/void data:write
POST /api/v1/gift-cards/{id}/adjust data:write
GET /api/v1/invoices?order_id=... data:read
POST /api/v1/invoices data:write
GET /api/v1/invoices/{id} data:read
GET /api/v1/invoices/{id}/pdf data:read
GET /api/v1/shop/export data:read
GET /api/v1/public/invoices/{id}?s=... the signed link from the buyer's email

These routes are not in the OpenAPI document yet; this page is their reference.

Hypamoon is the infrastructure your chat or agent builds on for a business. Hypamoon API, version 1.hypamoon.com