Codex · III

Documentation

Everything you need to wire Coin Moebius into a static site. Code samples, attribute reference, the workflow for each provider, and what every transaction status means.

Quick start

Three steps from signed-up to "live on your site." The dashboard's project page generates the exact snippets with your project ID baked in. The blocks below are illustrative.

  1. Paste the script tag once, in your site's <head>(or right before </body>). One copy is enough no matter how many buy buttons you have.
    <script src="https://sdk.coinmoebius.com/v1/coin-moebius-buy.js"
      crossorigin="anonymous"
      defer></script>
  2. Add the product to your catalog in the dashboard.Open your project, switch to the Products tab, click "Add product," and set the product-ref, name, price, and currency. By default Coin Moebius is in strict mode, which means the price you set here is the canonical price (a buyer cannot edit the price in their browser to pay less).
  3. Paste a buy button wherever you want it to appearin the page body. Reference the product by its product-ref in the product-id attribute. Repeat with a different product-id and label for each additional product.
    <coin-moebius-buy
      project-id="proj_YOUR_ID_HERE"
      product-id="t-shirt-medium"
      label="Buy a t-shirt">
    </coin-moebius-buy>
  4. Connect at least one provider in the dashboard.Open your project, switch to the Providers tab, click "Add provider," and follow the per-provider steps in Provider setup below.

The buy button

The buy button is a few lines of HTML you paste on any page. No framework, no install step on your end. The button renders on first paint; the provider picker and the payment-provider SDKs load on demand the first time a buyer clicks. It works in any site or framework that renders HTML, including the no-code site builders that let you paste HTML embeds.

Confirmed working with: Angular, Hugo, Jekyll, plain HTML, Webflow (Custom Code embed), Carrd (Embed element), Framer (HTML embed), Squarespace (Code Block), WordPress (Custom HTML block), Ghost, Notion sites (HTML embed widgets). If your site lets you paste HTML on a page, the buy button works there.

Attribute reference

AttributeRequiredWhat it acceptsNotes
endpointNoURL stringAlmost always omit this. The button auto-detects its API base: https://api.coinmoebius.com in production, http://localhost:8787 on localhost. Only set it as an override for an unusual setup (e.g., a self-hosted proxy in front of the API).
project-idYesString, prefixed proj_From your project page in the dashboard. Safe to expose publicly, it's an identifier, not a credential.
product-idYes (in strict mode) / Recommended (in ad-hoc mode)String, any format you chooseYour internal product identifier. In strict mode (the default), this is how the worker looks up the price from your catalog. In ad-hoc mode, it's still passed through to your webhooks in metadata.productId so you can map transactions back to inventory. See the Strict mode vs ad-hoc mode section below.
amountOnly in ad-hoc modeDecimal number as stringThe price in major units (e.g., dollars, not cents). Must be positive. Omit this in strict mode. The worker reads the canonical price from your catalog and ignores any value in the HTML. Required only when the project is in ad-hoc mode and the product-id is not in the catalog.
currencyOnly in ad-hoc modeThree-letter ISO 4217 code or your own unit nameUSD, EUR, etc. for cards / crypto rails. The pay-by-mail provider accepts any string (e.g., GBK) because the merchant is the one settling the transaction. Same rule as amount: omit in strict mode, required in ad-hoc mode for products not in the catalog.
labelNoStringThe button text. Defaults to Buy. Use this to distinguish buttons on the same page.
customer-refNoString, any format you chooseAn opaque identifier for the buyer in your own system, like a logged-in user id. The button forwards it to the worker as metadata.customerRef, and the worker tags the transaction with it. Later you can ask "which of my users paid?" using your own id, without us holding any actual customer data. Omit for anonymous checkouts.
themeNodark or lightPicks the built-in color scheme for the button and popup together. Defaults to dark. Set theme="light" for the light scheme. Your CSS variables override either one. See the styling guide.
editable-amountNotrue or falseWhen true, the popup shows an amount field the buyer fills in (tips, donations, pay-what-you-want). Defaults to false, where the amount you set is fixed and no field appears. The payment buttons stay disabled until the amount is a positive number.

Multiple buttons on one page

The script registers the button once, then any number of instances can render on a page. Each instance is independent. The example below uses strict mode (the default): no amount or currency in the HTML, because the worker looks the price up from each product in your catalog.

<coin-moebius-buy
  project-id="proj_YOUR_ID"
  product-id="t-shirt-medium"
  label="T-shirt (medium)">
</coin-moebius-buy>

<coin-moebius-buy
  project-id="proj_YOUR_ID"
  product-id="mug-blue"
  label="Blue mug">
</coin-moebius-buy>

If your project is in ad-hoc mode (donation widgets, tip jars), each button also includes amount and currency. See Strict mode vs ad-hoc mode for the full picture.

Theming the button

The buy button renders inside a Shadow DOM, which means none of your page's existing CSS reaches into it. That's deliberate: it keeps the button looking the same on every site, and stops a stray * { box-sizing: ... } rule from breaking the picker modal. You style it through two surfaces from your own stylesheet: CSS custom properties for colors, fonts, and shape, and ::part() selectors for everything else. No JavaScript involved.

Every variable and part is documented on the styling guide, with live examples you can click and copy:

Open the interactive styling guide →

Pricing: fixed vs buyer-priced

Each product in your catalog is either fixed (the price you set is what gets charged) or buyer-priced (the buyer or the embedded HTML picks the amount). The choice is per product, not per project, so a single project can run a catalog of fixed-price downloads next to a tip jar and a "name your own price" donation page on the same site, with one webhook URL and one set of provider credentials.

We never accept a payment for a product that isn't listed in your catalog. There is no project-level "trust anything" escape hatch; every checkout must reference a product you configured.

Why this matters

If the worker trusted the amount attribute on every buy button, a buyer could open their browser's developer tools, edit amount="29.99" to amount="0.01", click Buy, and pay one cent for a $30 product. Fixed-mode products make that impossible.

Fixed mode (the safe default)

New products are created in fixed mode. You set the price in your dashboard's Products tab; the buy element on your site references the product by id. The worker reads the price from your catalog and ignores any amount or currency the buy element might have on it.

<coin-moebius-buy
  project-id="proj_YOUR_ID"
  product-id="t-shirt-medium"
  label="Buy a t-shirt">
</coin-moebius-buy>

A buyer editing the HTML cannot change the price. The catalog wins, every time.

Buyer-priced mode (for tip jars, donations, and pay-what-you-want)

Some products only make sense if the buyer picks the amount: donation widgets, tip jars, name-your-own-price pages. For those, set the product's Pricing to Buyer-priced in the product form. The dashboard shows a safety dialog first because it's a real change in behavior: anyone with browser developer tools can edit the amount before paying. The price you set on the product becomes the suggested default the buy element pre-fills.

<coin-moebius-buy
  project-id="proj_YOUR_ID"
  product-id="tip-jar"
  amount="5.00"
  currency="USD"
  label="Leave a tip">
</coin-moebius-buy>

The dashboard's per-product snippet generator outputs the right shape automatically, with the suggested amount filled in.

Switching a product between modes

Fixed and buyer-priced are toggled per product, in the product's form. Switching to buyer-priced opens a one-time safety dialog because it's the destructive direction; switching back to fixed is immediate. New checkouts use the new mode straight away; already-completed transactions are unaffected.

Subscriptions

Coin Moebius can sell recurring subscriptions on every fiat provider in your menu. The merchant configures the price and interval once on a product; the buy button on the site stays exactly the same. The payment provider runs the recurring billing, holds the card, retries failed renewals, and hosts the cancellation page. Coin Moebius relays the lifecycle events to your code.

Setting up a recurring product

In the dashboard's Products tab, set the Billing field to Monthly or Annual instead of One-time. An optional Free trial field appears for any trial days you want to grant before the first charge. Save the product.

The buy button HTML doesn't change. The same product-id="pro-plan" attribute works for one-time or recurring; the worker checks the product's billing setting at checkout time and routes through the provider's subscription API when applicable.

<coin-moebius-buy
  project-id="proj_YOUR_ID"
  product-id="pro-plan"
  label="Subscribe to Pro">
</coin-moebius-buy>

What you don't have to build

The provider runs the schedule. We don't store cards. We don't run cron jobs. We don't retry failed charges. We don't send dunning emails. We don't host a cancellation page. That entire side of the system lives inside Stripe (or whichever fiat provider you've connected). You're outsourcing the hard parts of recurring billing to the company that's already running them for millions of merchants.

Subscription events

Renewals and cancellations show up as webhook events your server can react to. Five normalized types cover the lifecycle:

EventWhen it fires
subscription.createdNew signup. Carries the first cycle's amount.
subscription.renewedA non-initial cycle succeeded. Extend access through the new period end.
subscription.payment_failedA cycle's card was declined. The provider's dunning retries on its schedule; you usually just log this for visibility.
subscription.canceledTerminal cancellation. The buyer canceled, dunning was exhausted, or the merchant canceled.
subscription.updatedStatus change, card update, plan change. Inspect the new status.

Identifying buyers without storing them

Coin Moebius is a payment router, not a customer database. We never store buyer emails, names, addresses, or the provider's internal customer ids. If your application has user accounts (most subscription apps do), pass your own opaque user id as customer-ref on the buy button. The button forwards it to the worker as metadata.customerRef, we thread it through to the provider, surface it back on every event, and store only that opaque string. To us it's meaningless; to you it's the foreign key into your own user system.

<coin-moebius-buy
  project-id="proj_YOUR_ID"
  product-id="pro-plan"
  customer-ref="user_bob_42">
</coin-moebius-buy>

When you need deeper buyer detail (email, card last-four, dispute notes), the dashboard gives every transaction a "View in Stripe" link. You click through to the provider, where the buyer record actually lives. We never duplicate it.

Cancellation: link out, don't build

Buyers cancel in the provider's hosted portal: the Stripe Customer Portal, the buyer's PayPal account page, and so on. The portal handles cancellation, card updates, receipt downloads, and plan changes, all UI you don't have to build. You can drop the buyer into the portal with one API call:

const res = await fetch(
  `https://api.coinmoebius.com/api/subscriptions/${projectId}/${subId}/portal-url`,
  { method: 'POST', body: JSON.stringify({ returnUrl: 'https://you.example/account' }) },
);
const { url } = await res.json();
window.location.assign(url);

Which providers support subscriptions today

Stripe and PayPal work end-to-end through the hosted buy button. Set a product to Monthly or Annual in your dashboard, paste the button on your page, and the click starts a real subscription. The provider runs the renewals.

Square and Authorize.Net don't work through the hosted buy button. Their subscription APIs need the buyer's card collected on your own page, using the provider's JavaScript widget (Square Web Payments SDK, Authorize.Net Accept.js). If you're willing to add that step, the rest of the system (webhook routing, dashboard, status endpoint, customer linking) works exactly the same way it does for Stripe and PayPal subscriptions. See the next section.

Crypto providers (NOWPayments) do not support recurring billing in this product. Recurring crypto is friction-heavy on every gateway we've evaluated; we'd rather ship nothing than ship a half-broken story for it.

Use it without the buy button

The buy button is a convenience layer. Underneath, Coin Moebius is a rented webhook plus a few JSON endpoints. If you'd rather build your own UI, run your own checkout flow, or plug Coin Moebius into something the button can't handle (Square or Authorize.Net subscriptions, a custom payment form, a server-side script, a mobile app), every endpoint the button uses is also callable from anything. No code changes on our side.

This is what's available:

EndpointWhat it does
POST /api/checkout/{provider}/{projectId}Start a checkout. POST { productId, metadata }; receive back whatever the provider needs (a redirect URL for Stripe, an approval URL for PayPal, a token for Authorize.Net Accept Hosted). Render it however you want.
POST /webhook/{provider}/{projectId}The provider posts here. We verify the signature, normalize the event, store it, count quota. Point the provider's webhook at this URL whether the original checkout came from our button or your own integration.
GET /status/{projectId}/{txId}Poll for the current state of a payment or subscription. Returns the same normalized shape the buy button gets.
POST /api/subscriptions/{projectId}/{subscriptionId}/portal-urlGenerate a provider-hosted portal URL for the buyer to manage their subscription. Works for any provider that has a portal.

Identify the buyer in your own system by passing metadata.customerRef on the checkout call. It threads through the provider and back on every webhook event, so you can join Coin Moebius's records to your own user database without us storing anything about the buyer.

Why someone uses this path: a static-site builder who wants to write their own button to match their site's design and just needs the webhook handled. A developer who wants to skip the button entirely and call the API from their own server. A merchant who wants to run Square or Authorize.Net subscriptions and is fine wiring up card collection themselves. The button is a starting point. The API is the actual product.

Listening for buyer events

The element fires three browser events. Listen with addEventListener on the element (or on document, the events bubble). All events are cancelable, calling event.preventDefault() stops the default flow.

EventWhen it firesDetail payload
cm-load-providersThe picker modal is about to ask the API for the list of providers configured on this project.Empty.
cm-checkout-startedThe buyer picked a provider and Coin Moebius is about to create a checkout session (Stripe / NOWPayments) or generate a reference code (manual).{ provider: 'stripe' | 'nowpayments' | 'manual', ... }
cm-errorSomething failed: network error, signature failure, no provider configured.{ error: Error }
document.addEventListener('cm-error', (event) => {
  console.error('Coin Moebius:', event.detail.error);
  // Show your own error UI, send to your analytics, etc.
});

There is no cm-success event in the buyer's browser. By the time the payment actually completes, the buyer has been redirected to the payment provider's hosted checkout (Stripe Checkout, NOWPayments invoice page). They return to your site via the success_url you configured (see the next section), and your server learns about the payment via the dashboard or by polling the /status endpoint.

The success and cancel URLs

When you connect Stripe or NOWPayments in the dashboard's Providers tab, you set two URLs:

  • Success URL, where the buyer lands after a successful payment. Stripe and NOWPayments append a query parameter (?session_id=... for Stripe, ?NP_id=... for NOWPayments) so your success page can identify which transaction completed. Most static sites just show a generic "Thanks, your payment is processing" message and rely on the dashboard for the source of truth.
  • Cancel URL, where the buyer lands if they back out before paying. Often the same as your cart page.

Both URLs are configured per-provider in the dashboard, the element doesn't need to know about them.

Provider setup

Stripe

  1. Sign in to dashboard.stripe.com.
  2. Publishable key: Developers → API keys → "Publishable key" (starts pk_live_ or pk_test_).
  3. Secret key: same page → "Secret key" → reveal (starts sk_live_ or sk_test_).
  4. Webhook signing secret: Developers → Webhooks → Add endpoint. The endpoint URL is shown on your project page in our dashboard, copy it from there. Subscribe the endpoint to checkout.session.completed, charge.refunded, and charge.dispute.created. Stripe shows the signing secret once after creation (starts whsec_).
  5. In our dashboard's Providers tab, click "Add provider" → Stripe, paste all three values plus your Success URL and Cancel URL, save.

Testing: Use Stripe's test-mode keys (they start with pk_test_ and sk_test_) to run test purchases with Stripe's test card numbers. No real money moves. You can also use the "Send a test event" button on the Transactions tab to create a sample Stripe row instantly without involving Stripe at all.

NOWPayments

  1. Sign in to account.nowpayments.io.
  2. API key: Account → Store Settings → "API key".
  3. IPN secret: same page → "IPN Secret Key".
  4. IPN callback URL: paste the webhook URL from our dashboard's project page into NOWPayments' "IPN callback URL" field.
  5. In our dashboard, click "Add provider" → NOWPayments, paste the API key and IPN secret plus your Success URL and Cancel URL, save.

Testing: NOWPayments does not offer a sandbox or test mode. To verify the full round trip (checkout, IPN callback, and transaction row in your dashboard), send a real payment using a low-fee coin (like TRX or LTC) at the minimum amount. You can check current minimums on the NOWPayments status page. A few cents confirms everything works end to end. If you want to see what a NOWPayments transaction looks like in your dashboard before spending anything, use the "Send a test event" button on the Transactions tab and pick NOWPayments as the provider.

Pay by mail (manual)

No external accounts, no API keys, no credentials. You provide a mailing address and Coin Moebius generates a unique reference code for each transaction. This works for cash, checks, money orders, Goldbacks, precious metals, or anything else a buyer can put in an envelope.

  1. In your dashboard, click "Add provider" → Pay by mail.
  2. Enter your mailing address. This is exactly what the buyer sees when they choose "Pay by mail" in the picker, so use the address where you actually receive packages.
  3. Enter the expected currency. This is free-form: type USD for cash or checks, GBK for precious metals, or whatever unit makes sense for what you're accepting. You're the one opening the envelope, so you decide what counts.
  4. Save. The buyer can now pick "Pay by mail" in the picker alongside any other providers you've connected (Stripe, NOWPayments, etc.) and get a unique reference code like X2M-K9P-R7QW to include with their shipment.

You don't need separate buy buttons for different payment methods. The picker handles the choice. A single button on your page can offer cards, crypto, and pay-by-mail at once.

Transaction statuses

Each transaction row in the dashboard has one of these statuses. The status reflects the most recent event for that transaction; refunds and disputes update the existing row rather than inserting a sibling.

StatusMeaning
succeededPayment completed. Money is in your account (net of provider fees).
pendingPayment is in flight. Common with async payment methods (ACH, some crypto rails) where confirmation takes minutes to hours.
failedPayment did not complete: card declined, expired auth, hard rejection. No money moved.
partialBuyer paid less than invoiced (common when a crypto buyer sends a network-fee-reduced amount). The row's amount reflects what was actually received; check metadata.invoicedAmount for what was requested.
refundedMoney has been returned to the buyer, fully or partially. The row's amount is the refunded amount; refunds can happen days or weeks after the original payment.
disputedThe buyer (or their bank) opened a dispute or chargeback. Check the provider's interface for the response window. The row's metadata.reason carries the provider's classification verbatim.
pending_manualA pay-by-mail transaction is awaiting your physical confirmation. See the next section.
manual_canceledYou clicked Cancel on a pending pay-by-mail row before the buyer's payment arrived.
manual_expiredA pending pay-by-mail row sat for 30 days without confirmation and auto-expired.
manual_revokedYou confirmed receipt of a pay-by-mail payment but later undid the confirmation (e.g., the payment turned out to be invalid). The row moves from succeeded back to this terminal state.

The pay-by-mail flow

  1. The buyer clicks the Coin Moebius button on your site and picks "Pay by mail" in the picker. They see a preview first: your mailing address, the amount due, and any instructions you set, with a <strong>"Confirm, I'll send payment"</strong> button. Nothing is recorded yet, and there is no reference code at this stage.
  2. The buyer clicks "Confirm, I'll send payment." That is the step that creates the transaction: Coin Moebius generates a unique reference code like X2M-K9P-R7QW and shows it alongside your mailing address. The buyer puts the reference code inside the package or on the check.
  3. Once they confirm, a row appears in your dashboard's Transactions tab with status pending_manual. The top of the list also shows a callout reminding you that mailed-payment confirmations are waiting.
  4. When the buyer's payment arrives in your mailbox (cash, check, Goldback, whatever the expected currency was), type the reference code into the search box on the Transactions tab to find the matching row. Click Mark received. The buyer's success URL is fired (so they get a confirmation page), and you proceed to ship whatever they bought.
  5. If the buyer's payment never arrives, the row auto-expires after 30 days. You can also Cancel a row manually before that.
  6. If you confirmed receipt but later discover the payment was invalid (counterfeit, wrong item, empty envelope), click Undo confirmation on the row. The status changes to manual_revoked. This corrects your records in the dashboard. Communicating with the buyer about what happened next is between you and them.

Testing pay-by-mail

Pay-by-mail has no sandbox, no test keys, and no external account to configure. The entire flow happens between your site and your dashboard. There are two ways to test it: the full round trip from a buy button on a page, or a quick test straight from the dashboard.

Quick test from the dashboard

The dashboard can create a test transaction for any provider without involving a real payment. This is useful for seeing what a transaction row looks like, practicing the "Mark received" flow for pay-by-mail, or previewing how a crypto payment appears in your list before spending real money.

  1. Open the Transactions tab on your project and click Send a test event.
  2. Pick a product, choose what happened (payment, refund, pay-by-mail, subscription renewal), and pick the provider you want to simulate (Stripe, NOWPayments, or Pay by mail). For pay-by-mail events, the provider is set automatically.
  3. Click Send test event. A new row appears tagged "test" so it doesn't mix with live data. The row shows up under the correct provider filter, just like a real transaction would.

For pay-by-mail specifically, choose "A pay-by-mail order is waiting" to create a pending_manual row with a real reference code. You can then practice clicking "Mark received," "Cancel," and "Undo confirmation" on it.

Test events don't count toward your monthly transaction quota.

Full round trip from a buy button

To test the complete flow (what your buyer sees through what you see), walk through these steps. The whole thing takes about two minutes.

  1. Set up the provider. In your dashboard, go to your project's Providers tab and click "Add provider" → Pay by mail. Enter any mailing address (your own is fine for testing) and any currency (e.g., USD, GBK). Save.
  2. Add a product. In the Products tab, create a product with a name like "Test item" and a price. Set the currency to match what you entered for the provider.
  3. Put the buy button on a page. Paste the buy button HTML on any page, even a bare HTML file on your computer. Point the endpoint attribute at your Cloud project URL. If you're running the worker locally, use http://localhost:8787 instead.
  4. Click the button and pick "Pay by mail." The picker opens showing every provider you've connected. Pick "Pay by mail." You'll see a preview with your mailing address, the expected amount and currency, and a "Confirm, I'll send payment" button. No reference code yet.
  5. Click "Confirm, I'll send payment." This is the buyer committing to send payment. Coin Moebius records the transaction and shows a reference code like X2M-K9P-R7QW.
  6. Open the dashboard. Go to the Transactions tab. A new row appears with status pending_manual and the same reference code you just saw. If this is your first pending row, a callout at the top of the list reminds you that mailed payments are waiting for confirmation.
  7. Click "Mark received." The row's status changes to succeeded. That's the complete happy path, from the buyer's click to your confirmation.

Finding a transaction by reference code

When a payment arrives in your mailbox, look for the reference code the buyer included (printed on the envelope, written on the check memo, or tucked inside the package). Then type that code into the search box on the Transactions tab and press Enter. The table filters to matching rows so you can click "Mark received" on the right one without paging through your full transaction history.

Testing the other outcomes

Run through the same steps again, but instead of marking the row received, try each of these:

  • Cancel it. Click "Cancel" on the pending row. The status changes to manual_canceled. This is what you'd do when a buyer tells you they changed their mind before mailing anything.
  • Let it expire. Leave a pending row alone. After 30 days it auto-expires to manual_expired. You don't need to wait 30 days to verify the behavior. Expiration works the same as Cancel, except it happens automatically on a schedule.
  • Enter a different amount. When clicking "Mark received," enter a received amount that differs from what was expected (e.g., the buyer sent 4 Goldbacks instead of 5). The transaction still succeeds, but the dashboard shows both the expected and received amounts so your records reflect what actually arrived.
  • Undo a confirmation. First mark a row received so it shows succeeded, then click "Undo confirmation" on that row. The status changes to manual_revoked. This is how you'd correct a mistake or flag a payment that turned out to be invalid after the fact.

What to look for

  • The reference code in the picker matches the reference code on the dashboard row.
  • Your mailing address displays correctly in the picker.
  • The "Mark received" and "Cancel" buttons appear only on pending_manual rows, not on card or crypto transactions.
  • After marking received, the row shows succeeded with the correct amount.
  • Searching by reference code in the search box finds the right row.

Full round-trip transactions created during testing count toward your monthly quota. On the free tier (100 transactions/month), a handful of test runs won't make a dent. Test events sent from the dashboard's "Send a test event" button don't count at all.

Refunds, disputes, partial payments

Coin Moebius listens for these provider events and updates the original transaction row in the dashboard. Specifically:

  • Refunds: Stripe's charge.refunded event (full or partial refunds, including amount_refunded so partial refunds show the slice that was returned). NOWPayments' refunded IPN. The row's status flips to refunded, the amount reflects the refunded amount, and metadata.originalChargeId / metadata.originalAmount carry the original payment context.
  • Disputes (chargebacks): Stripe's charge.dispute.created event. The row's status flips to disputed. The provider's stated reason passes through to metadata.reason verbatim, you can render it in your own UI however you choose.
  • Partial payments: NOWPayments' partially_paid IPN. The row's status flips to partial. The amount reflects what was actually received (actually_paid); metadata.invoicedAmount is what was requested.

In v1, these events are surfaced in the dashboard and via the /status endpoint. A future release will add email notifications and an outbound webhook forwarder so your own backend can react automatically. For now, set up a small polling job (see the next section) or check the dashboard.

Polling from your backend

For a server-side source of truth, poll GET /status/:projectId/:txId from your backend. The response shape:

{
  "status": "succeeded",
  "amount": 29.99,
  "currency": "USD",
  "isTest": false,
  "createdAt": "2026-05-14T01:04:21.000Z",
  "updatedAt": "2026-05-14T01:04:21.000Z"
}

Status values follow the same enum as the dashboard (see Transaction statuses). The endpoint is unauthenticated but rate-limited to 60 requests / minute per IP. The transaction id you pass is whatever the SDK returned in the cm-checkout-started event or what's shown in the dashboard's Reference column.

A typical pattern: when your success_url page loads, kick off a backend job that polls /status/:projectId/:txId every 15 seconds until it sees succeeded (or failed / a timeout), then fulfill the order.