Skip to content

The Musqet developer platform

Musqet has a REST API so your own software can do what the dashboard does: read your transactions and settlements, create and manage payment links, run a terminal sale, and more. It can also call you back: register a webhook and Musqet will POST to your server the moment a payment is created or completed.

There are two sides to this, and you will use both.

  • In My Musqet you create the API keys your code authenticates with, and you register the webhook endpoints Musqet delivers to. This is point-and-click, and it is where the secrets live.
  • From your own code you send requests with your key and receive webhooks on your endpoint. That is the rest of this page.

Every endpoint lives under the /api/v1 prefix. Rather than list them all here, where a copy would drift from the code, Musqet publishes the catalogue live:

  • The OpenAPI document is the machine-readable OpenAPI 3 description of every endpoint, its inputs and its responses, for generating a client, importing into Postman or Insomnia, or reading from an agent.
  • The interactive reference is the same document rendered for a person, with examples, and shows the base URL to use.

The examples below use two shell variables so you can paste them as-is once you have set them:

Terminal window
export MUSQET_API_URL="…" # the base URL shown at the top of /docs
export MUSQET_API_KEY="msqt_api_live_…" # created in My Musqet, see below

An API key is the credential your code presents on every request. Musqet stores only a hash of the key, never the key itself, so a key is shown to you once at the moment you create it and can never be retrieved again. It carries a set of permissions, so a key can be scoped to only what a given integration needs.

In My Musqet, open API keys and create one. You give it:

  • a name, so you can tell your keys apart later,
  • a preset, which fills in the permissions a known integration needs so you do not have to know them yourself. Pick your integration (for example Odoo) and its permissions are selected for you. Pick Custom to choose the permissions yourself.
  • the permissions it should hold, chosen from your business’s permission set. See Permissions below for what each one allows.
  • optionally an expiry date, after which the key stops working on its own.

The new key is shown in full one time, prefixed msqt_api_live_. Copy it into your secret store there and then. If you lose it, you cannot see it again: revoke it and make a new one.

A key carries a set of permissions, and every endpoint requires one. If a key does not hold the permission an endpoint needs, the request comes back 403. Grant a key only the permissions its integration actually uses.

Each permission has a name you see in My Musqet, a code you pass in the API’s permissions array (and see quoted in a 403), and the actions it allows.

In My MusqetPermissionWhat it lets a key do
View salessales:readRead your sales and settlement history.
View Bitcoin backendbitcoin-backend:readRead your Bitcoin node and wallet configuration.
Manage Bitcoin backendbitcoin-backend:writeChange your Bitcoin node and wallet configuration.
View paymentstake-payments:readRead your payments and hosted checkouts.
Take paymentstake-payments:writeCreate hosted checkouts and take payments.
View tip linkstip-links:readRead your tip links.
Manage tip linkstip-links:writeCreate and delete tip links.
Issue refundsrefunds:writeIssue refunds against sales.
Withdraw fundswithdraw:writeMove funds out of your Musqet balance.
View backupbackup:readRead your wallet backup data.
View devicesdevices:readRead your enrolled terminals and their settings.
Manage devicesdevices:writeEnrol, configure and remove terminals.
View teamteam:readRead your team members and their roles.
Manage teamteam:writeInvite, edit and remove team members.
View API keysapi-keys:readList the API keys on your business.
Manage API keysapi-keys:writeCreate and revoke API keys.
View webhookswebhooks:readList your webhook endpoints and their delivery history.
Manage webhookswebhooks:writeCreate, edit and remove webhook endpoints, and rotate their signing secrets.
Customise dashboarddashboard:writeChange your dashboard layout.
Verify UCP Lightning preimagesucp:verifyVerify Lightning payment preimages.
Manage notification recipientsnotifications:writeManage who receives your notifications.
View terminal salesterminal-sales:readRead sales taken on a payment terminal.
Take terminal salesterminal-sales:writeStart, poll and cancel sales on a payment terminal.

The Odoo preset selects View devices (devices:read), View terminal sales (terminal-sales:read) and Take terminal sales (terminal-sales:write): what the point-of-sale integration needs to resolve a terminal by serial and run a sale.

If a key leaks, or an integration is retired, revoke it from the same API keys screen. Revocation takes effect immediately: the very next request that presents it is rejected. Revoking one key does not touch your others.

Present your key as a bearer token in the Authorization header:

Terminal window
curl "$MUSQET_API_URL/api/v1/transactions" \
-H "Authorization: Bearer $MUSQET_API_KEY"

Musqet hashes the key you send, looks it up, and checks that it is not revoked and not past its expiry. If it fails any of those, the request comes back 401. If the key is valid but does not hold the permission an endpoint requires, the request comes back 403. The key also decides which business the request acts on, so you never pass a business id yourself.

Two things are worth knowing:

  • The key is a static bearer token, not a signature. Send it only over HTTPS, and keep it server-side. There is no request-signing step on the calling side.
  • The checkout endpoints are the exception. POST /api/v1/checkouts and reading or subscribing to a checkout need no API key, because they are meant to be driven from a customer’s browser where a secret key must never appear. Everything else under /api/v1 needs a key. See Payment links and hosted checkout for that flow.

Every error comes back as JSON in one shape:

{
"error": {
"code": 401,
"message": "Invalid or inactive API key.",
"metadata": {}
}
}

code is the HTTP status, repeated in the body so it survives logging. message is a human-readable reason. metadata is present only on some errors and carries structured detail.

StatusMeaningWhat to do
400The request body or query failed validationFix the input; the message names the field
401Missing, malformed, revoked or expired keyCheck the Authorization header and the key’s state
403The key is valid but lacks the required permissionGrant the permission to the key, or use one that has it
404The thing you asked for does not existCheck the id in the path
429You are being rate limitedBack off and retry after a short wait
500Something failed on our sideRetry; if it persists, contact support

A webhook lets Musqet call you. You register a URL, and Musqet POSTs a small JSON body to it when an event you asked for happens, so you do not have to poll. This is the outbound direction and is entirely separate from the callbacks Musqet receives from card and bitcoin processors.

In My Musqet, open Webhooks and add one. You give it:

  • the URL to POST to (it must be a public HTTPS address in production; private and internal addresses are refused),
  • the one event it should receive, and
  • an optional description.

An endpoint listens for a single event type. To receive more than one event, add an endpoint per event, pointing at the same URL. When you create an endpoint, Musqet shows you its signing secret once, prefixed whsec_. Store it: you need it to verify deliveries, and like an API key it cannot be retrieved again.

There are four:

EventFires when
payment.createdA payment has been started for one of your checkouts
payment.completedThat payment has completed
terminal.sale.completedA sale on a payment terminal completed
terminal.sale.failedA sale on a payment terminal failed

Every delivery has the same body:

{
"eventType": "payment.completed",
"checkoutId": "…",
"externalId": "your-own-reference-or-null",
"status": "…",
"amount": 0,
"currency": "GBP",
"network": null,
"txIds": [],
"createdAt": "2026-08-12T10:00:00.000Z"
}

externalId is the reference you set when you created the checkout, so you can tie the event back to your own order. network and txIds are populated for bitcoin payments and are null or empty otherwise.

Anyone can POST to a public URL, so verify the signature before you trust a delivery. Each request carries these headers:

HeaderHolds
x-webhook-signaturet=<timestamp>,v1=<hmac>
X-Webhook-TimestampThe Unix timestamp used in the signature
X-Webhook-EventThe event type
X-Webhook-DeliveryThe delivery id

The signature is an HMAC-SHA256, keyed with your endpoint’s signing secret, over the string <timestamp>.<raw-body>. Recompute it from the raw request body (not a re-serialised copy of the parsed JSON) and compare in constant time:

import { createHmac, timingSafeEqual } from 'node:crypto'
const verify = (rawBody, header, secret) => {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
// Reject a delivery whose timestamp is too old to be genuine, so a captured
// request cannot be replayed later. Widen or narrow the window to suit you.
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false
const expected = createHmac('sha256', secret)
.update(`${parts.t}.${rawBody}`, 'utf8')
.digest('hex')
const a = Buffer.from(expected)
const b = Buffer.from(parts.v1 ?? '')
return a.length === b.length && timingSafeEqual(a, b)
}

Reject anything that does not match. The freshness check rejects a delivery whose timestamp is more than five minutes old, so a request captured off the wire cannot be replayed against you later.

If a secret leaks, rotate it from the endpoint’s page in My Musqet. You get a fresh whsec_ secret, shown once. Update your verification code to the new value.

Each endpoint keeps a delivery history in My Musqet: what was sent, the response status your server returned, and whether it succeeded or failed. Use it when an event does not arrive.

Musqet retries a failed delivery for you. A delivery counts as successful only on a 2xx response. A non-2xx, a timeout (deliveries time out after 30 seconds) or a network error is retried with an increasing delay, for up to five attempts in total (the first delivery and four retries). If an endpoint fails ten deliveries in a row, Musqet disables it and stops sending, so a dead URL does not pile up forever. Re-enable it once your server is healthy again.

The kiosk API is a separate, smaller surface, under /kiosk/v1, for driving a Musqet payment terminal from your own kiosk or till software: fetch the business and terminal details for a serial, start a sale, then poll or cancel it.

It authenticates differently from the main API. Instead of a bearer key, a kiosk request carries an x-api-key header, and the value is the kiosk key issued for that installation rather than one of the per-business API keys above. The endpoints are listed in the live reference alongside the rest.

SymptomLikely causeFix
Every request returns 401Key is wrong, revoked or expired, or the header is malformedConfirm the header is Authorization: Bearer msqt_api_live_… and the key is active in My Musqet
A request returns 403The key lacks the permission that endpoint needsGrant the permission to the key, or use one that has it
A webhook never arrivesThe endpoint is for a different event, or a previous run of failures disabled itCheck the event type on the endpoint and its delivery history in My Musqet
The signature check failsVerifying against a re-serialised body, or against the wrong secretSign the raw request body, and confirm you are using this endpoint’s current whsec_ secret
A webhook stopped after a bad deployTen consecutive failures disabled the endpointFix your server, then re-enable the endpoint