Open Sandbox and click the button that generates your sandbox. Wait about 8 minutes, until the sandbox is ready. Details
Then copy the 3 values of Resource-owner credentials: Email, Password, and Company ID.
3Create your credential
When the sandbox is ready, the portal creates a sandbox credential named API Sandbox Quickstart. It is authorized, with the authorization_code and password grant flows. Open Credentials → API credentials, open this credential, and copy its Client ID and Client Secret. You can also create your own sandbox credential: it is authorized immediately.
4Set your environment variables
Paste the values of steps 2 and 3 in your terminal:
Copy the value of access_token into a variable. It is valid for 2 hours:
shell
export MOONCARD_TOKEN="<access_token>"
Without a scope parameter, the token gets all the scopes of the credential. With it, send a subset of these scopes: the token gets only these. Got invalid_client or invalid_grant? Check the values of step 4. Password flow reference
A demo company to test with no risk for real customers.
Contents: the company Acme Corporate (validated KYB, company account), users with roles (you are the owner), 12 cards, and about 5 months of expenses and accounting data.
Wait about 8 minutesThe status refreshes by itself. After 30 minutes, the portal shows a failure and a retry.
Ready-to-use credentials come with your sandbox. When the sandbox is ready, the portal also creates two sandbox credentials: API Sandbox Quickstart and MCP Sandbox Quickstart. They are already authorized, with the authorization_code and password grant flows. Copy the client ID and secret of API Sandbox Quickstart in Credentials → API credentials, then follow Your first API call (password grant).
The portal creates them once, whatever your other credentials. Details
Sign in
The Sandboxes page shows your resource-owner credentials: Email, Password, and Company ID, each with a copy button. Use the email and the password:
in the password flow (grant_type=password),
on the OAuth consent screen (/oauth/authorize).
The sandbox web app is not open to the public. To see the Mooncard web app, contact Mooncard support at support@mooncard.co to request a demo.
Reset or delete
Reset
New demo company. Your test data is lost.
Delete
No more sandbox. You can create one again later.
Not possible during the generation. Mooncard can remove a sandbox with no activity for 6 months.
Onboarding API · Step 3 of 6
Create your credential
A credential gives you a client ID and a client secret for the OAuth 2 authentication. In the sandbox, use API Sandbox Quickstart, or create your own.
Just generated your sandbox? When the sandbox is ready, the portal creates API Sandbox Quickstart and MCP Sandbox Quickstart for you. You can use them immediately: no need to create one.
You can also create your own credential before the sandbox generation ends. The portal creates the quickstart credentials in all cases.
Portal: Credentials → New credential.
Field
What to enter
Editable
Type
API REST
No
Name
Users see it on the consent screen.
Yes
Usage context
Own company: you use the integration for your own Mooncard account only. Third-party account: you make a partner integration that different clients can use. Payment initiation account: you are an AISP or a PISP, and you make the Mooncard connector for Open Banking.
No
Description
Your use case and why you need the data. 1,000 characters max.
Yes
Redirect URI
Your callback URL, for example https://your-app.example/oauth/callback. One URI per line. Required with the authorization_code grant flow: after the sign-in, Mooncard sends the user back to this URI with the code.
Yes
Grant flows
The OAuth flows that the credential can use: authorization_code and password. Without a grant flow, the credential cannot get a token. Flows
Yes
Scopes
Only what you use. A credential with no scopes cannot get a token. Reference
sequenceDiagram
autonumber
actor User
participant App as Your integration
participant MC as Mooncard
User->>App: Clicks "Connect Mooncard"
App->>MC: Redirects the browser to /oauth/authorize
User->>MC: Signs in and accepts the scopes
MC->>App: Redirects to your redirect URI with code and state
App->>MC: POST /oauth/token with the code and the client secret
MC->>App: access_token and refresh_token
App->>MC: GET /api/v3/... with Authorization: Bearer
redirect_uri must match a registered URI exactly. state is a random value that you keep in the user session.
scope is optional. Without it, the token gets all the scopes of the credential. With it, send a subset of these scopes: the token gets only these. A scope that the credential does not have gives invalid_scope. A credential with no scopes cannot get a token.
Refresh before expiry or on a 401. Replace the old refresh token with the new one. Without scope, the new token keeps the scopes of the previous token. With it, send a subset of these scopes. A refresh cannot add a scope.
Password flow
One request, no browser. Use it when the user and the company do not change. API Sandbox Quickstart allows it. On your own credential, select password in the Grant flows.
Copy username, password, and company_id from Sandbox → Resource-owner credentials (fields Email, Password, and Company ID). company_id selects the user profile. Without it, the API refuses most calls.
scope is optional. Without it, the token gets all the scopes of the credential. With it, send a subset of these scopes: the token gets only these. A credential with no scopes cannot get a token.
Browse the API in your browser
Portal: Documentation → API reference, or Browse the API on a sandbox API credential. Sandbox only. It uses your latest sandbox API credential by default, and you can select another one.
Copy the client secretClick the credential name at the top of the page. The explorer never shows the secret.
Click AuthorizePaste the secret, select the scopes, and sign in with your sandbox email and password.
Try it out → ExecuteThe page shows the curl command, the status, and the response.
Problem
Fix
Redirect URI error
Add https://developers.mooncard.co/oauth2_redirect to the credential.
Mooncard sends an HTTPS request to your server when an expense changes. This page tells you how to receive the request, how to verify its signature, and what Mooncard does when your server fails.
Always verify the signature. Every request carries a signature made with a secret that only you and Mooncard know. Reject each request that does not have a valid signature. Your webhook URL is public, so anybody can send a request to it. Verify the signature
How webhooks work
A webhook is an HTTPS URL and a list of events. A user of your company owns the webhook. Mooncard sends the events of this company to the URL, but only the expenses that the owner can read.
The owner sees and changes the webhook. A super admin sees and changes every webhook of the company. The other users see only the webhooks that they own, in the app and with the API.
CreatePOST /api/v3/webhooks with the HTTPS URL, the events, and the payload format. A new webhook is inactive. Keep the secret of the answer.
TestPOST /api/v3/webhooks/{id}/test. The test must get a 2xx answer from your URL.
ActivatePUT /api/v3/webhooks/{id} with status set to active. Only after a successful test.
When you change the URL or the payload format, the webhook becomes inactive again. Test it again, then activate it.
The URL starts with https:// and leads to a public address. Mooncard refuses localhost and the private, loopback, and link-local addresses. Mooncard checks the address of the host name again at each request, and does not call a host name that leads to such an address.
Your company can test its webhooks 10 times a minute. After that, Mooncard refuses the test with a 422 answer. Wait a minute, then test again.
Choose the payload format
Each webhook has a payload format. Set it when you create or change the webhook: on the webhook page in the app, or with the payload_format parameter of the API.
Format
Content type
Use
json
application/json
Recommended for a new integration. The official Standard Webhooks libraries verify it with no extra code.
form
application/x-www-form-urlencoded
The default format, and the format of every webhook made before the json format existed.
Both formats have the same fields, the same headers, and the same signature.
The request
Mooncard sends a POST request. The body has these fields:
Field
Value
id
The identifier of the delivery. It is the same on every retry of the same delivery.
company_id
The identifier of your company.
webhook_id
The identifier of the webhook.
event
The event, for example expense.update.
resource
The expense, in the format of the Mooncard API v3: a JSON object in the json format, a JSON string in the form format. Mooncard makes it when it sends the request, not when the event occurs. For the example event, the value is {}.
The request also has three signature headers:
Header
Value
webhook-id
The identifier of the delivery. It is equal to the id field.
webhook-timestamp
The time of this attempt, in seconds since the Unix epoch.
webhook-signature
One or more signatures, separated by a space. Each signature has the format v1,<base64>.
Answer with a 2xx status code. Any other status code, a timeout, or a connection error is a failure.
Answer in less than 10 seconds. Verify the signature, store the event, answer, and then do the work in the background.
Mooncard does not follow redirects: a 3xx answer is a failure. Give the final URL.
Answer 410 Gone when you removed the endpoint and want no more requests. Mooncard then switches the webhook off immediately and sends an email to its owner.
Verify the signature
Mooncard signs each request with the Standard Webhooks method. To verify a request, do these steps:
Read the raw bytes of the body. Do not parse the JSON or the form before you verify the signature: a parsed and encoded body can be different from the bytes that Mooncard signed.
Make sure that the three headers are present. Make sure that webhook-timestamp is less than 300 seconds before or after your current time. This stops an attacker who sends an old request again.
Remove the whsec_ prefix from your secret. Decode the remaining text from base64: the result is the signing key.
Make the signed content: <webhook-id>.<webhook-timestamp>.<raw body>, with a full stop between the parts.
Calculate the HMAC-SHA256 of the signed content with the signing key. Encode the result in base64.
Split webhook-signature on the space character. Remove the v1, prefix from each signature. The request is valid when one signature is equal to your result. Use a constant-time comparison.
After a secret rotation, the header has two signatures for 24 hours: one with the new secret and one with the old secret. The request is valid with either secret. Do not expect a fixed number of signatures.
The json format: use the Standard Webhooks libraries
The official libraries do all the steps above, including the timestamp check. They return the parsed body. Give them the raw body and the request headers.
Install the library, then verify each request in Rails.
Gemfile
gem 'standardwebhooks'
ruby
class MooncardWebhooksController < ActionController::API
def create
webhook = StandardWebhooks::Webhook.new(ENV.fetch('MOONCARD_WEBHOOK_SECRET'))
payload = webhook.verify(request.raw_post, request.headers)
# Ignore a webhook-id that you already processed, store the event, process it later.
expense = payload['resource']
head :no_content
rescue StandardWebhooks::WebhookVerificationError
head :unauthorized
end
end
Install the library, then verify each request in Express.
shell
npm install standardwebhooks
express.raw keeps the raw body.
javascript
const express = require('express');
const { Webhook } = require('standardwebhooks');
const app = express();
const webhook = new Webhook(process.env.MOONCARD_WEBHOOK_SECRET);
app.post('/mooncard/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
let payload;
try {
payload = webhook.verify(req.body, req.headers);
} catch (error) {
return res.sendStatus(401);
}
// Ignore a webhook-id that you already processed, store the event, process it later.
const expense = payload.resource;
res.sendStatus(204);
});
app.listen(3000);
Install the library, then verify each request in Flask.
shell
pip install standardwebhooks
python
import os
from flask import Flask, request
from standardwebhooks.webhooks import Webhook, WebhookVerificationError
app = Flask(__name__)
webhook = Webhook(os.environ["MOONCARD_WEBHOOK_SECRET"])
@app.post("/mooncard/webhooks")
def mooncard_webhook():
try:
payload = webhook.verify(request.get_data(), request.headers)
except WebhookVerificationError:
return "", 401
# Ignore a webhook-id that you already processed, store the event, process it later.
expense = payload["resource"]
return "", 204
The form format: verify the signature with your own code
The Ruby library does not accept the form format. After the verification, the Ruby library always parses the body as JSON, so it rejects a valid form request. The Node.js and Python libraries accept a form request only with the jsonParse: false or json_parse=False option. Use the code below, or change the webhook to the json format.
These functions work with both formats.
This function works in any Ruby application. The controller shows how to use it in Rails.
ruby
require 'base64'
require 'openssl'
class InvalidWebhook < StandardError; end
def verify_mooncard_webhook(secret, msg_id, timestamp, signature_header, raw_body)
raise InvalidWebhook, 'Missing header' if [msg_id, timestamp, signature_header].any? { |value| value.to_s.empty? }
raise InvalidWebhook, 'Timestamp too old or too new' if (Time.now.to_i - timestamp.to_i).abs > 300
key = Base64.strict_decode64(secret.delete_prefix('whsec_'))
expected = Base64.strict_encode64(OpenSSL::HMAC.digest('SHA256', key, "#{msg_id}.#{timestamp}.#{raw_body}"))
signatures = signature_header.split(' ').filter_map { |entry| entry.delete_prefix('v1,') if entry.start_with?('v1,') }
raise InvalidWebhook, 'Invalid signature' unless signatures.any? { |value| OpenSSL.secure_compare(value, expected) }
end
ruby
class MooncardWebhooksController < ActionController::API
def create
verify_mooncard_webhook(
ENV.fetch('MOONCARD_WEBHOOK_SECRET'),
request.headers['webhook-id'],
request.headers['webhook-timestamp'],
request.headers['webhook-signature'],
request.raw_post
)
# Ignore a webhook-id that you already processed, store the event, process it later.
expense = JSON.parse(params.require(:resource))
head :no_content
rescue InvalidWebhook
head :unauthorized
end
end
This function works in any Node.js application. The handler shows how to use it with Express: express.raw keeps the raw body.
javascript
const crypto = require('node:crypto');
function verifyMooncardWebhook(secret, msgId, timestamp, signatureHeader, rawBody) {
if (!msgId || !timestamp || !signatureHeader) throw new Error('Missing header');
const age = Math.floor(Date.now() / 1000) - Number(timestamp);
if (!(Math.abs(age) <= 300)) throw new Error('Timestamp too old or too new');
const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
const expected = crypto
.createHmac('sha256', key)
.update(`${msgId}.${timestamp}.`)
.update(rawBody)
.digest();
const valid = signatureHeader.split(' ').some((entry) => {
const [version, value] = entry.split(',');
if (version !== 'v1' || !value) return false;
const received = Buffer.from(value, 'base64');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
});
if (!valid) throw new Error('Invalid signature');
}
module.exports = { verifyMooncardWebhook };
javascript
const express = require('express');
const { verifyMooncardWebhook } = require('./verify');
const app = express();
app.post('/mooncard/webhooks', express.raw({ type: 'application/x-www-form-urlencoded' }), (req, res) => {
try {
verifyMooncardWebhook(
process.env.MOONCARD_WEBHOOK_SECRET,
req.get('webhook-id'),
req.get('webhook-timestamp'),
req.get('webhook-signature'),
req.body,
);
} catch (error) {
return res.sendStatus(401);
}
const fields = new URLSearchParams(req.body.toString('utf8'));
// Ignore a webhook-id that you already processed, store the event, process it later.
const expense = JSON.parse(fields.get('resource'));
res.sendStatus(204);
});
app.listen(3000);
This function works in any Python application. The view shows how to use it with Flask.
python
import base64
import hashlib
import hmac
import time
def verify_mooncard_webhook(secret, msg_id, timestamp, signature_header, raw_body):
if not (msg_id and timestamp and signature_header):
raise ValueError("Missing header")
if abs(int(time.time()) - int(timestamp)) > 300:
raise ValueError("Timestamp too old or too new")
key = base64.b64decode(secret.removeprefix("whsec_"))
signed = f"{msg_id}.{timestamp}.".encode() + raw_body
expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest())
for entry in signature_header.split(" "):
version, _, value = entry.partition(",")
if version == "v1" and hmac.compare_digest(value.encode(), expected):
return
raise ValueError("Invalid signature")
python
import json
import os
from urllib.parse import parse_qs
from flask import Flask, request
from verify import verify_mooncard_webhook
app = Flask(__name__)
@app.post("/mooncard/webhooks")
def mooncard_webhook():
raw_body = request.get_data()
try:
verify_mooncard_webhook(
os.environ["MOONCARD_WEBHOOK_SECRET"],
request.headers.get("webhook-id"),
request.headers.get("webhook-timestamp"),
request.headers.get("webhook-signature"),
raw_body,
)
except ValueError:
return "", 401
fields = parse_qs(raw_body.decode())
# Ignore a webhook-id that you already processed, store the event, process it later.
expense = json.loads(fields["resource"][0])
return "", 204
The secret
Each webhook has its own secret. The secret starts with whsec_.
The webhook page in the Mooncard app shows the secret.
The API gives the secret only in the answer to the creation (POST /api/v3/webhooks) and to the rotation (POST /api/v3/webhooks/{id}/secret). GET requests do not give the secret.
Keep the secret on your server only. Do not put it in source code, in a log, or in a browser.
To replace the secret, rotate it in the app or with the API. For 24 hours, Mooncard signs each request with the new secret and with the old secret. Deploy the new secret on your server during these 24 hours, and you lose no event.
Retries and failures
When an attempt fails, Mooncard pauses the webhook and sends the same delivery again after these delays:
Attempt
Delay after the previous attempt
2
1 minute
3
5 minutes
4
30 minutes
5
2 hours
6
5 hours
7 to 10
10 hours
11
12 hours
Mooncard makes a maximum of 11 attempts for one delivery, over approximately 2.5 days.
While the webhook is paused, new events wait. When your server answers again, Mooncard sends the waiting events one at a time.
Mooncard sends a maximum of one request at a time to each webhook.
After the last failed attempt, Mooncard switches the webhook off. The deliveries that wait also fail. To start again, test the webhook and activate it.
When the owner of the webhook can no longer read the expense, Mooncard stops the delivery.
Email when a webhook is switched off. Mooncard sends an email to the owner of the webhook: the user who created it. Mooncard does not send this email to the company administrators, or to an owner whose account is archived or deactivated. The email gives the URL, the event that failed, the last error, the number of attempts, and a link to the webhook page.
Each delivery has a log: its status (pending, succeeded, or failed), the number of attempts, the HTTP status code, the error, the duration, the first kilobyte of your answer, and the time of the next attempt. The webhook page in the app shows the latest logs and lets you send a failed delivery again. The API gives all the logs, and can also send a failed delivery again.
When the last attempt got an HTTP answer, the log gives its status code and the error is empty. Otherwise, the error tells why:
Error
Meaning
Net::OpenTimeout
Mooncard could not connect to your server in 10 seconds.
Net::ReadTimeout
Your server did not answer in 10 seconds.
Errno::ECONNREFUSED
Your server refused the connection.
SocketError
The host name of the URL does not resolve.
OpenSSL::SSL::SSLError
The TLS connection failed, for example because of an invalid certificate.
forbidden_address
The host name leads to a private, loopback, or link-local address. Mooncard did not call it.
resource_not_readable
The owner can no longer read the expense, or the account of the owner is archived or deactivated. Mooncard stopped the delivery.
webhook_inactive
The webhook was inactive when the delivery was due.
webhook_disabled
Mooncard switched the webhook off after the last failed attempt of another delivery, while this delivery was waiting.
Other connection errors show the name of their error class.
Duplicates and order
Mooncard sends each delivery at least once. Your server can receive the same delivery two times, for example when your answer is lost. Keep the webhook-id values that you processed, and ignore a delivery that you already processed.
Mooncard does not guarantee the order of the deliveries. When the order is important, use the resource field only as a notification, and get the current expense with GET /api/v3/expenses/{id}.
Manage webhooks with the API
The webhook endpoints are part of the Mooncard API v3. They need the webhook:read scope to read and the webhook:write scope to change. See the webhook API reference for the parameters and the answers of each endpoint.
Change a webhook. Set status to active to activate it after a successful test. A change of url or payload_format makes the webhook inactive and untested.
Send a failed delivery again. The webhook must be active. A delivery logged before the delivery logs kept the expense cannot be sent again: the answer is 404.
Onboarding API · Production
Go to production
When your tests in the sandbox work, create a production credential. Mooncard reviews it before your integration can reach real Mooncard data. Sandbox credentials do not work in production.
Sign in to create a production credential in the portal (Documentation → Production guide).
From sandbox to production
Create a production credentialPortal: Credentials → New credential, mode Production, type API REST. Describe the purpose of the credential, select only the scopes that it needs, and set its grant flows and redirect URI. Fields
Wait for the Mooncard reviewThe credential stays pending until Mooncard authorizes it. You get an email with the decision.
Connect real Mooncard accountsOnly Mooncard users can grant your credential access to their data. The sandbox email and password do not work in production. Who signs in
Switch your configurationUse https://app.mooncard.co, and the client ID and client secret of the production credential. Details
Production scopes are final. You cannot change the scopes or the usage context of a production credential after its creation. Select them carefully before you submit. To change them, contact Mooncard support at support@mooncard.co.
Sandbox and production
Sandbox
Production
Host
https://sandbox.mooncard.co
https://app.mooncard.co
Data
Your demo company
Real customers
Review
None: authorized immediately
Mooncard review
Who signs in
Your sandbox user
Mooncard users with a production account
Grant flows
authorization_code, password
authorization_code, password
Redirect URI
HTTP or HTTPS
HTTPS only
Scopes
Editable
Final after creation
Usage context
Final after creation
Final after creation
Checklist before you submit
Your integration works in the sandbox, with the flow that you use in production.
An HTTPS redirect URI, if you use the authorization_code flow.
The client secret on a server only.
Restricted scopes (for example virtual card numbers): contact Mooncard before you submit.
Review
pending → Mooncard reviews (and can contact you) → authorized or rejected. While the credential is pending, Mooncard refuses its token requests.
When Mooncard authorizes or rejects your credential, you get an email at the address of your developer account. Questions about the review? Contact api@mooncard.co.
Accounts that you need
Step
What you need
Create the production credential
Only your developer account. You do not need a Mooncard customer account.
Get a production token
1. Mooncard reviewed and authorized the credential. 2. A user signs in with a Mooncard production account that has a profile in the target company.
Usage context
Who signs in
Own company
You or a colleague, with your own production account. Your company must already be a Mooncard customer.
Third-party account
Your customers, with their own production accounts. Each customer connects their own company.
Payment initiation account
The Mooncard customers that use your AISP or PISP service, with their own production accounts.
No production account, no token. If your company is not a Mooncard customer, the Own company context cannot work in production. Contact Mooncard before you submit the credential.
Switch your configuration
When Mooncard authorizes the credential, change these values in your server configuration:
Authorization code flow: send the users to https://app.mooncard.co/oauth/authorize, with a redirect URI registered on the production credential. Flow
Password flow: use the email and the password of a Mooncard production account. Ask your account manager for the production company_id: the sandbox company_id does not match it. Flow
Sandbox tokens do not work on https://app.mooncard.co. Get new tokens with the production credential.
Onboarding API
Good practices and security
Good practices
per_page=200 for large lists.
Webhooks instead of polling.
Reuse the token until it expires.
Security checklist
No secret, token, or sandbox password in source control.
A random state in each authorization, checked on return.
Refresh tokens stored encrypted.
Rotate the secret if it leaks.
Minimum scopes.
Onboarding API
Scope reference
:read reads, :write changes. The role of the user also limits the token.
Sandbox sign-in fails (password flow or consent screen)
Use the sandbox email and password, not your portal password. Wait for "ready".
See the Mooncard web app
The sandbox web app is not public. Contact Mooncard support at support@mooncard.co to request a demo.
unauthorized_client
Credential pending, or flow not allowed. Check the Grant flows of the credential.
Redirect URI error
Exact match: scheme, host, path, trailing slash.
403 role_permission
The user role is too low. In the sandbox, use the owner.
Webhook stopped
11 failed attempts over about 2.5 days, or a 410 answer. The owner gets an email. Fix your server, read the delivery logs, then test and activate the webhook again. Retries
Webhook URL refused, or forbidden_address in the logs
The URL must use HTTPS and lead to a public address. Set up a webhook
Cannot change the scopes
Production scopes are final after creation. Contact Mooncard support at support@mooncard.co.
Onboarding MCP · Private beta
Onboarding MCP
Let an AI assistant read and act on your Mooncard data. Same API, same credentials, same permissions.
OAuth, with the client ID and secret of your MCP credential
The assistant never sees more than you. It acts as the signed-in user, in this user's company, with this user's role.
Private beta. In production, new MCP credentials join a waitlist: Mooncard reviews each one before it can be used. Sandbox credentials are authorized immediately, with no review. Connecting sends Mooncard data to your AI provider: read the terms.
The sandbox is a demo Mooncard company. Your AI client signs in to it with your sandbox user.
GeneratePortal: Sandbox, then click the button that generates your sandbox. Wait about 8 minutes, until the sandbox is ready.
Confirm the email of the sandbox userMooncard creates a sandbox company and a user with your email. Confirm this email address from the message that Mooncard sends you. Until you confirm it, your AI client cannot sign in.
Keep the sign-in valuesSandbox → Resource-owner credentials shows the Email and the Password. Your AI client asks for them when it connects.
When the sandbox is ready, the portal also creates MCP Sandbox Quickstart. Sandbox details
An MCP credential gives your AI client a client ID and a client secret. In the sandbox, use MCP Sandbox Quickstart, or create your own.
Just generated your sandbox? The portal creates MCP Sandbox Quickstart for you. It is authorized immediately. Open Credentials → MCP credentials, open it, and copy its Client ID and Client Secret.
Create your own sandbox MCP credential
Same form as the API credential. Portal: Credentials → MCP credentials, or Documentation → MCP guide → New MCP credential.
Field
Value for MCP
Type
MCP (beta)
Name
For example Acme — Claude MCP.
Usage context
Usually Own company.
Description
What the assistant will do, for example month-end clean-up.
Redirect URI
Required.https://mcp-sandbox.mooncard.co/oauth/callback, the callback of the sandbox MCP server. The portal fills it in for a new MCP credential.
Grant flows
authorization_code only.
Scopes
Only the tools you need. :read for reporting only. Reference
Production needs an authorized credential and a production account. Your production MCP credential must be approved by Mooncard, and you sign in with a Mooncard production account of your company. Go to production
Advanced settingsPaste the client ID and the client secret.
ConnectSign in with your Mooncard production account, then approve.
Receipt uploads (optional)Organization Settings → Capabilities → Code execution → Domain allowlist: add the domain below. On Team and Enterprise plans, an owner does it.
Ask in plain language. See what the server does not do in Limits and security. The assistant finds the right tools and acts on each item instead of only listing it.
Work through your to-do list
Read everything that Mooncard waits for from you: expenses to complete or correct, virtual card requests, a disabled card. Then act on each item.
“What is waiting for me in Mooncard? Handle what you can and tell me what needs a decision.”
Match receipts, fill the gaps
Attach your receipts to their expenses, and create the expenses that exist only as a receipt.
“Attach my receipts to the matching expenses, and create the ones that are missing.”
Approve a batch, with your own rule
Select exactly what you want to validate, by team, category, amount, and period, and move it through the workflow in one go.
“Approve my team's travel expenses under €100 from last month.”
Chase missing receipts
Find who has no receipt and follow up: a comment and a correction request to the owner of the expense.
“Notify the people who have expenses without a receipt last month.”
Flag a wrong receipt
Comment on the expense and send its owner a correction request, so that the person who can fix it gets it.
“Add a comment and send a correction request on the expenses whose receipt does not match.”
Answer a spend question
Filter expenses by person, team, period, supplier, or category. Read account balances, movements, and declined payments, with no export and no pivot table.
“How much did my employees cost in hotels and trains this year while working remotely?”
Chart a trend
Get the expenses of a period and let your assistant turn them into the chart or the table that you asked for.
“Show me the curve of our AI spend since January.”
Onboarding MCP
Tools reference
More than 150 tools. Your assistant picks them by itself.
Area
Covers
Expenses
Search, read, create, update all expense types. Record a supplier payment.
When your tests in the sandbox work, create a production MCP credential. In production, new MCP credentials join a waitlist: Mooncard reviews each one before your AI client can reach real Mooncard data. Sandbox credentials do not work in production.
Sign in to create a production MCP credential in the portal (Documentation → MCP guide).
From sandbox to production
Create a production MCP credentialSame form as in the sandbox, with the production values below. Values
Get it approvedThe credential stays pending until Mooncard authorizes it. You get an email with the decision. Review
Connect your AI client againWith the production server URL and the client ID and secret of the production credential. Sign in with your Mooncard production account. Steps
You need a Mooncard production account to connect. Your AI client signs in with a production account that has a profile in your company. Your company must already be a Mooncard customer, with the Advanced Control pack. The sandbox email and password do not work in production. Accounts that you need
Production scopes are final. You cannot change the scopes or the usage context of a production credential after its creation. To change them, contact Mooncard support at support@mooncard.co.
The other fields are the same as in the sandbox: type MCP (beta), grant flow authorization_code only, and only the scopes that you need. Fields
Get it approved
pending → Mooncard reviews (and can contact you) → authorized or rejected. You get an email with the decision, at the address of your developer account.
Same authentication, permissions, and terms as the API. Plus these rules.
No
Money movement: no transfer, no card payment. Recording a supplier invoice as paid is the only payment-related write.
No
Onboarding, contract, pricing, or billing operations.
No
Access to another company. One connection = the company of the signed-in user.
You confirm
Sensitive or irreversible actions. One approval = one action.
EU
Server hosted in the EU. No data stored: only the short session and technical logs, with no payload and no personal data.
Your conversation and the data it contains stay with your AI provider, under its terms.
Onboarding MCP
Terms and support
Read before you connect. Share with your users.
A third party receives your data. Expenses, amounts, suppliers, names, and receipts go to your AI provider. Mooncard does not control it.
Its terms apply. Check its privacy, retention, and training terms, and your company policy, before you connect.
The connector can write. It creates, changes, and deletes real data. Some actions are not reversible from the assistant. Read what you approve.
No money moves. No transfer, no payment, no card transaction.
Assistants make mistakes. Check each figure before you use it.
Not professional advice. Not accounting, tax, legal, or financial advice.
Content can carry instructions. Text in titles, comments, or receipts can steer the assistant. Decline actions that you did not ask for.
You own the access. Choose the scopes and who gets the credentials. Delete the credential to revoke all access.
You can stop at any time. Remove the connector: your connection ends. Delete the credential: it ends for everyone.
Beta. Tools and this page can change. No extra availability or support commitment. Your Mooncard contract, Terms of Service, and Privacy Policy still apply.
Support
In-app chat or support@mooncard.co. Include:
Your client_id (never the secret)
The AI client and account
Date, time, and time zone
Your request, the failed tool, and the error
API reference
Introduction
Explore and use the endpoints of the Mooncard API v3.
Own company (your Mooncard account only), Third-party account (a partner integration for different clients), or Payment initiation account (AISP or PISP, Open Banking connector). Choose well: you cannot change it later.