Developers
Sign in

Mooncard Developers

Welcome to Mooncard Developers

Connect your software or your AI assistant to Mooncard data: cards, expenses, users, and settings.

Useful links

Developer Portaldevelopers.mooncard.co
Sandbox (API)https://sandbox.mooncard.co
Production (API and web app)https://app.mooncard.co
API version/api/v3
API referenceAll endpoints, with code examples

Onboarding API

Onboarding API

Make your first call to the Mooncard API in about 15 minutes, on a demo company. No review and no Mooncard customer account are necessary.

Start hereYour first API call in 6 stepsFrom sign-up to GET /api/v3/me on your sandbox, with commands to copy. →

What you get

  • A sandbox: the demo company Acme Corporate, with users, cards, and 5 months of expenses.
  • A ready credential: API Sandbox Quickstart, already authorized.
  • The full API: same scopes, same OAuth flows, same /api/v3 as in production.

6 steps to your first API call

  1. Create your accountOn developers.mooncard.co. Step 1
  2. Generate your sandboxAbout 8 minutes. Copy the Email, Password, and Company ID. Step 2
  3. Get your credentialUse API Sandbox Quickstart, or create your own. Step 3
  4. Set your environment variablesHost, client ID and secret, email, password, company ID. Quickstart
  5. Get an access tokenOne request with the password flow. Step 5
  6. Call the APIGET /api/v3/me, then your expenses. Step 6

When it works

Onboarding API · Quickstart

Your first API call (password grant)

Follow these 6 steps in order. At the end, you call the Mooncard API on your sandbox from a terminal.

You need: a terminal with curl, and about 15 minutes. Most of it is the sandbox generation.

1Create your account

Sign up at developers.mooncard.co and confirm your email address. Details

2Generate your sandbox

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:

shell
export MOONCARD_HOST="https://sandbox.mooncard.co"
export MOONCARD_CLIENT_ID="<Client ID>"
export MOONCARD_CLIENT_SECRET="<Client Secret>"
export MOONCARD_EMAIL="<Email>"
export MOONCARD_PASSWORD="<Password>"
export MOONCARD_COMPANY_ID="<Company ID>"

5Get an access token

The password flow gives a token in one request, with no browser:

shell
curl -X POST "$MOONCARD_HOST/oauth/token" \
  -u "$MOONCARD_CLIENT_ID:$MOONCARD_CLIENT_SECRET" \
  --data-urlencode "grant_type=password" \
  --data-urlencode "username=$MOONCARD_EMAIL" \
  --data-urlencode "password=$MOONCARD_PASSWORD" \
  --data-urlencode "company_id=$MOONCARD_COMPANY_ID"

Mooncard answers with a token:

json
{
  "access_token": "ACCESS_TOKEN",
  "token_type": "Bearer",
  "expires_in": 7200,
  "refresh_token": "REFRESH_TOKEN",
  "scope": "public",
  "created_at": 1760947200
}

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

6Call the API

Get the user profile of the token:

shell
curl "$MOONCARD_HOST/api/v3/me" \
  -H "Authorization: Bearer $MOONCARD_TOKEN"

Then list the first 5 expenses of your demo company:

shell
curl -i "$MOONCARD_HOST/api/v3/expenses?page=1&per_page=5" \
  -H "Authorization: Bearer $MOONCARD_TOKEN"

-i shows the headers: X-Total-Count gives the total number of expenses.

It works. Next steps:

Onboarding API · Step 1 of 6

Create your account

Your developer account gives you access to the sandbox, the credentials, and the API reference. There is no manual review.

  1. Sign upOpen developers.mooncard.co and sign up with your work email. Disposable addresses are refused.
  2. Confirm your emailClick the link in the confirmation email.
  3. Sign inYou land on the portal home page.

Work with colleagues? You can share your credentials later. See Work as a team.

Next: generate your sandbox →

Onboarding API · Step 2 of 6

Generate your sandbox

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.

Generate it

  1. Open the Sandboxes pagedevelopers.mooncard.co/sandboxes
  2. Click GenerateOne sandbox per account.
  3. 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

ResetNew demo company. Your test data is lost.
DeleteNo 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.

FieldWhat to enterEditable
TypeAPI RESTNo
NameUsers see it on the consent screen.Yes
Usage contextOwn 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
DescriptionYour use case and why you need the data. 1,000 characters max.Yes
Redirect URIYour 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 flowsThe OAuth flows that the credential can use: authorization_code and password. Without a grant flow, the credential cannot get a token. FlowsYes
ScopesOnly what you use. A credential with no scopes cannot get a token. ReferenceSandbox only
ModeSandbox. For production, see Go to production.No

After creation, the page shows the client ID and the client secret.

Redirect URI rules

  • Absolute URI, no fragment (#…).
  • HTTPS in production. HTTP is accepted in the sandbox.
  • One URI per line.
  • Required with the authorization_code grant flow: without it, the authorization cannot redirect the user back.

A sandbox credential is authorized immediately: you can get tokens at once.

The client secret

  • Server only. Never in a mobile app, front-end code, or a public repository.
  • Rotate secret: the old secret stops immediately. Update your configuration first.
  • Delete the credential to revoke all its tokens.

Onboarding API · Steps 4–5 of 6

Get an access token

A token acts for one user in one company. The API returns only what this user can see.

Step 4 first: set your environment variables, as in Your first API call (password grant).

Full request and response reference: API reference → Authentication.

In the sandbox, sign in with your sandbox user. For production, see who signs in.

Your needFlowWhere
Users connect their Mooncard accountAuthorization codeSandbox and production
No browser: a fixed user and companyPasswordSandbox and production
Try endpoints by handBrowse the APISandbox only
EndpointUse
GET /oauth/authorizeThe user signs in and accepts the scopes.
POST /oauth/tokenGet or refresh a token.
POST /oauth/revokeCancel a token.

Authorization code flow

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
      

1. Send the user to Mooncard

url
https://sandbox.mooncard.co/oauth/authorize
  ?response_type=code
  &client_id=<CLIENT_ID>
  &redirect_uri=https%3A%2F%2Fyour-app.example%2Foauth%2Fcallback
  &scope=me%3Aread%20expense%3Aread
  &state=<RANDOM_VALUE>

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.

2. Receive the code

url
https://your-app.example/oauth/callback?code=<CODE>&state=<RANDOM_VALUE>
  • Different state: stop.
  • error=access_denied: the user refused.
  • The code works one time only.

3. Exchange the code (server side)

shell
curl -X POST https://sandbox.mooncard.co/oauth/token \
  -d grant_type=authorization_code \
  -d code=<CODE> \
  -d redirect_uri=https://your-app.example/oauth/callback \
  -d client_id=<CLIENT_ID> \
  -d client_secret=<CLIENT_SECRET>
json
{
  "access_token": "…",
  "token_type": "Bearer",
  "expires_in": 7200,
  "refresh_token": "…",
  "scope": "me:read expense:read",
  "created_at": 1760947200
}

The access token lasts 2 hours (expires_in: 7200). Store the refresh token encrypted, on your server.

4. Call the API

shell
curl https://sandbox.mooncard.co/api/v3/me \
  -H "Authorization: Bearer <ACCESS_TOKEN>"

Refresh the token

shell
curl -X POST https://sandbox.mooncard.co/oauth/token \
  -d grant_type=refresh_token \
  -d refresh_token=<REFRESH_TOKEN> \
  -d client_id=<CLIENT_ID> \
  -d client_secret=<CLIENT_SECRET>

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.

shell
curl -X POST https://sandbox.mooncard.co/oauth/token \
  -d grant_type=password \
  -d username=<SANDBOX_USER_EMAIL> \
  -d password=<SANDBOX_USER_PASSWORD> \
  -d company_id=<SANDBOX_COMPANY_ID> \
  -d client_id=<CLIENT_ID> \
  -d client_secret=<CLIENT_SECRET>

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.

  1. Copy the client secretClick the credential name at the top of the page. The explorer never shows the secret.
  2. Click AuthorizePaste the secret, select the scopes, and sign in with your sandbox email and password.
  3. Try it out → ExecuteThe page shows the curl command, the status, and the response.
ProblemFix
Redirect URI errorAdd https://developers.mooncard.co/oauth2_redirect to the credential.
403Missing scope. Authorize again with it.
401Token expired (2 hours). Logout, then Authorize.

Revoke a token

shell
curl -X POST https://sandbox.mooncard.co/oauth/revoke \
  -d token=<ACCESS_OR_REFRESH_TOKEN> \
  -d client_id=<CLIENT_ID> \
  -d client_secret=<CLIENT_SECRET>

Token errors

ErrorFix
invalid_clientWrong client ID or secret. Copy them again. Did you rotate the secret?
unauthorized_clientCredential not authorized, or flow not allowed. Check the Grant flows of the credential.
invalid_grantCode or refresh token wrong, expired, or used. Or redirect_uri differs. Start again.
invalid_scopeThe credential does not have this scope. Send a subset of the scopes of the credential, or no scope.
invalid_redirect_uriURI not registered. Check scheme, host, path, and trailing slash.

Onboarding API · Step 6 of 6

Call the API

JSON endpoints under /api/v3. Send the token in the Authorization header.

shell
curl https://sandbox.mooncard.co/api/v3/me \
  -H "Authorization: Bearer <ACCESS_TOKEN>"

Start with GET /api/v3/me (scope me:read): it confirms that your token works.

Pagination

pageStarts at 1.
per_pageDefault 30, max 200. 0 returns only the total.
X-Total-CountResponse header: the total number of items.

Errors

StatusCauseFix
400Missing or malformed parameterRead the message.
401Token missing, expired, or revokedRefresh the token.
403Missing scope or role (see reason)Add the scope, or use a user with the correct role.
404Not found, or not visible to this userCheck the ID and the company.
422Business rule not metCorrect the listed fields.
json
{ "error": "Forbidden", "reason": "role_permission" }

Onboarding API

Webhooks

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.

EventMooncard sends it when
expense.createAn expense is created.
expense.updateAn expense changes.
expense.deleteAn expense is deleted.
exampleYou test the webhook. The resource is empty.

Set up a webhook

Create the webhook in the Mooncard app or with the API (Manage webhooks with the API).

  1. 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.
  2. TestPOST /api/v3/webhooks/{id}/test. The test must get a 2xx answer from your URL.
  3. 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.

FormatContent typeUse
jsonapplication/jsonRecommended for a new integration. The official Standard Webhooks libraries verify it with no extra code.
formapplication/x-www-form-urlencodedThe 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:

FieldValue
idThe identifier of the delivery. It is the same on every retry of the same delivery.
company_idThe identifier of your company.
webhook_idThe identifier of the webhook.
eventThe event, for example expense.update.
resourceThe 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:

HeaderValue
webhook-idThe identifier of the delivery. It is equal to the id field.
webhook-timestampThe time of this attempt, in seconds since the Unix epoch.
webhook-signatureOne or more signatures, separated by a space. Each signature has the format v1,<base64>.

An example of a request in the json format:

http
POST /mooncard/webhooks HTTP/1.1
Content-Type: application/json
webhook-id: 6f1c2d0e-8a4b-4c3e-9f5a-2b7d1e0c9a84
webhook-timestamp: 1793001600
webhook-signature: v1,<base64 HMAC-SHA256>

{"id":"6f1c2d0e-8a4b-4c3e-9f5a-2b7d1e0c9a84","company_id":"<company id>","webhook_id":"<webhook id>","event":"expense.update","resource":{"id":"<expense id>",...}}

The same request in the form format:

http
POST /mooncard/webhooks HTTP/1.1
Content-Type: application/x-www-form-urlencoded
webhook-id: 6f1c2d0e-8a4b-4c3e-9f5a-2b7d1e0c9a84
webhook-timestamp: 1793001600
webhook-signature: v1,<base64 HMAC-SHA256>

id=6f1c2d0e-8a4b-4c3e-9f5a-2b7d1e0c9a84&company_id=<company id>&webhook_id=<webhook id>&event=expense.update&resource=%7B%22id%22%3A%22<expense id>%22%2C...%7D

Answer the request

  • 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:

  1. 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.
  2. 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.
  3. Remove the whsec_ prefix from your secret. Decode the remaining text from base64: the result is the signing key.
  4. Make the signed content: <webhook-id>.<webhook-timestamp>.<raw body>, with a full stop between the parts.
  5. Calculate the HMAC-SHA256 of the signed content with the signing key. Encode the result in base64.
  6. 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

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

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:

AttemptDelay after the previous attempt
21 minute
35 minutes
430 minutes
52 hours
65 hours
7 to 1010 hours
1112 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:

ErrorMeaning
Net::OpenTimeoutMooncard could not connect to your server in 10 seconds.
Net::ReadTimeoutYour server did not answer in 10 seconds.
Errno::ECONNREFUSEDYour server refused the connection.
SocketErrorThe host name of the URL does not resolve.
OpenSSL::SSL::SSLErrorThe TLS connection failed, for example because of an invalid certificate.
forbidden_addressThe host name leads to a private, loopback, or link-local address. Mooncard did not call it.
resource_not_readableThe owner can no longer read the expense, or the account of the owner is archived or deactivated. Mooncard stopped the delivery.
webhook_inactiveThe webhook was inactive when the delivery was due.
webhook_disabledMooncard 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.

EndpointScopeUse
GET /api/v3/webhookswebhook:readList the webhooks.
POST /api/v3/webhookswebhook:writeCreate a webhook. Set payload_format to json or form (the default). The answer contains the secret.
GET /api/v3/webhooks/{id}webhook:readGet a webhook.
PUT /api/v3/webhooks/{id}webhook:writeChange 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.
DELETE /api/v3/webhooks/{id}webhook:writeDelete a webhook.
POST /api/v3/webhooks/{id}/testwebhook:writeSend the example event once, with no retry. 10 tests a minute for each company. The old GET method still works, but it is deprecated.
POST /api/v3/webhooks/{id}/secretwebhook:writeRotate the secret. The answer contains the new secret.
GET /api/v3/webhooks/{id}/logswebhook:readList the deliveries, newest first, with pagination.
POST /api/v3/webhooks/{id}/logs/{log_id}/retrywebhook:writeSend 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

  1. 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
  2. Wait for the Mooncard reviewThe credential stays pending until Mooncard authorizes it. You get an email with the decision.
  3. 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
  4. 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

SandboxProduction
Hosthttps://sandbox.mooncard.cohttps://app.mooncard.co
DataYour demo companyReal customers
ReviewNone: authorized immediatelyMooncard review
Who signs inYour sandbox userMooncard users with a production account
Grant flowsauthorization_code, passwordauthorization_code, password
Redirect URIHTTP or HTTPSHTTPS only
ScopesEditableFinal after creation
Usage contextFinal after creationFinal after creation

Checklist before you submit

  • Your integration works in the sandbox, with the flow that you use in production.
  • Minimum scopes only. See the scope reference.
  • A clear description: use case, data, and why. Mooncard uses it for the review.
  • The correct usage context. See who signs in.
  • 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

StepWhat you need
Create the production credentialOnly your developer account. You do not need a Mooncard customer account.
Get a production token1. 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 contextWho signs in
Own companyYou or a colleague, with your own production account. Your company must already be a Mooncard customer.
Third-party accountYour customers, with their own production accounts. Each customer connects their own company.
Payment initiation accountThe 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:

shell
export MOONCARD_HOST="https://app.mooncard.co"
export MOONCARD_CLIENT_ID="<Production Client ID>"
export MOONCARD_CLIENT_SECRET="<Production Client Secret>"
  • 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.


ScopeAccess

Onboarding API

Troubleshooting

ProblemFix
Sandbox stays in "generating"Wait 8 minutes. After 30 minutes, click retry. Fails again: contact Mooncard.
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 appThe sandbox web app is not public. Contact Mooncard support at support@mooncard.co to request a demo.
unauthorized_clientCredential pending, or flow not allowed. Check the Grant flows of the credential.
Redirect URI errorExact match: scheme, host, path, trailing slash.
403 role_permissionThe user role is too low. In the sandbox, use the owner.
Webhook stopped11 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 logsThe URL must use HTTPS and lead to a public address. Set up a webhook
Cannot change the scopesProduction 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.

Server URLProduction: https://mcp.mooncard.co/mcp
Sandbox: https://mcp-sandbox.mooncard.co/mcp
AuthenticationOAuth, 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.

What you can do with it

Once connected, your assistant can:

Clear your to-do list

Complete expenses, match receipts, chase the missing ones, and send correction requests.

Approve in batch

Control, approve, or reject expenses with your own rule: team, category, amount, period.

Manage cards

Freeze, activate, change limits, and handle virtual card requests.

Answer spend questions

Filter expenses, read balances and declined payments, and chart a trend.

See all use cases →

MCPAPI
Ad-hoc, in conversation: month-end clean-up, spend questions, card operations, receipts.A product: ERP or accounting connector, scheduled sync, webhooks.

Requirements

  • The Advanced Control pack (ask your Account Manager).
  • An AI client with remote MCP and OAuth: Claude, ChatGPT, Mistral, Cursor…

What you get in the sandbox

  • A sandbox: the demo company Acme Corporate, with users, cards, and 5 months of expenses.
  • A ready credential: MCP Sandbox Quickstart, already authorized, with no review.
  • The sandbox MCP server: https://mcp-sandbox.mooncard.co/mcp. Same tools as in production.

4 steps to your first MCP connection

  1. Create your accountOn developers.mooncard.co. Step 1
  2. Generate your sandboxAbout 8 minutes. Confirm the email of your sandbox user. Step 2
  3. Get an MCP credentialUse MCP Sandbox Quickstart, or create your own. Step 3
  4. Connect your AI clientThen try the use cases on demo data. Step 4

When it works

Onboarding MCP · Step 2 of 4

Generate your sandbox

The sandbox is a demo Mooncard company. Your AI client signs in to it with your sandbox user.

  1. GeneratePortal: Sandbox, then click the button that generates your sandbox. Wait about 8 minutes, until the sandbox is ready.
  2. 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.
  3. 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

Next: get an MCP credential →

Onboarding MCP · Step 3 of 4

Get an MCP credential

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.

FieldValue for MCP
TypeMCP (beta)
NameFor example Acme — Claude MCP.
Usage contextUsually Own company.
DescriptionWhat the assistant will do, for example month-end clean-up.
Redirect URIRequired. 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 flowsauthorization_code only.
ScopesOnly the tools you need. :read for reporting only. Reference
ModeSandbox: authorized immediately, with no review.

Sandbox connection values

Server URLhttps://mcp-sandbox.mooncard.co/mcp
Client ID and client secretThe page of your sandbox MCP credential
Receipt upload domain (Claude)mooncard-mcp-sandbox.s3.eu-central-1.amazonaws.com

The portal shows these values, ready to copy, in Documentation → MCP guide.

Next: connect your AI client →

Onboarding MCP · Step 4 of 4

Connect your AI client

Your AI client runs the OAuth flow. You only paste 3 values.

Server URLhttps://mcp.mooncard.co/mcp
Client ID and secretThe page of your production MCP credential. Sign in to see it.
Receipt upload domain (Claude)mooncard-mcp-production.s3.eu-central-1.amazonaws.com

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

  1. Settings → Connectors → Add custom connectorName Mooncard, URL https://mcp.mooncard.co/mcp.
  2. Advanced settingsPaste the client ID and the client secret.
  3. ConnectSign in with your Mooncard production account, then approve.
  4. Receipt uploads (optional)Organization Settings → Capabilities → Code execution → Domain allowlist: add the domain below. On Team and Enterprise plans, an owner does it.
domain
mooncard-mcp-production.s3.eu-central-1.amazonaws.com

Menu labels can change between client versions. The fields stay the same.

Try it

Ask your assistant a first question:

“What is waiting for me in Mooncard? Handle what you can and tell me what needs a decision.”

More examples: Capabilities and use cases.

Onboarding MCP

Capabilities and use cases

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.

AreaCovers
ExpensesSearch, read, create, update all expense types. Record a supplier payment.
CardsSettings, limits, assignment, freeze, activation, PIN, termination, temporary overrides.
Virtual cardsRequest, list, approve, decline.
People and accessProfiles, collaborators, roles, departments, contacts.
ReceiptsList, download, create from a file, attach, detach, delete.
AccountsBalances, movements, top-up bank details (also as PDF).
Payment errorsDeclined authorizations and their reason.
Validation workflowWhat waits for you. Control, approve, reject, verify.
Comments and correctionsComments, correction requests and their withdrawal.
Company and settingsExpense form setup, currencies, e-invoicing address, user rights.
Field suggestionsCategory, analytic code, cost center, title, attendees.
Accounting reference dataCategories, codes, axes, suppliers, vehicles, VAT rates, policies…
NotificationsTask feed, mark as read.

Onboarding MCP · Production

Go to production

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

  1. Create a production MCP credentialSame form as in the sandbox, with the production values below. Values
  2. Get it approvedThe credential stays pending until Mooncard authorizes it. You get an email with the decision. Review
  3. 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.

Production values

SandboxProduction
ModeSandboxProduction
Server URLhttps://mcp-sandbox.mooncard.co/mcphttps://mcp.mooncard.co/mcp
Redirect URI (required)https://mcp-sandbox.mooncard.co/oauth/callbackhttps://mcp.mooncard.co/oauth/callback
Receipt upload domain (Claude)mooncard-mcp-sandbox.s3.eu-central-1.amazonaws.commooncard-mcp-production.s3.eu-central-1.amazonaws.com
ReviewNoneWaitlist and Mooncard review
Who signs inYour sandbox userA Mooncard production account of your company
ScopesEditableFinal after creation

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.

To go faster, send Mooncard support:

  • Company and Mooncard account
  • Credential name and client_id
  • AI client and number of users
  • Intended use: reporting, month-end clean-up, card operations…

Once authorized: connect your AI client with the production values. General production rules: Go to production (API).

Onboarding MCP

Limits and security

Same authentication, permissions, and terms as the API. Plus these rules.

NoMoney movement: no transfer, no card payment. Recording a supplier invoice as paid is the only payment-related write.
NoOnboarding, contract, pricing, or billing operations.
NoAccess to another company. One connection = the company of the signed-in user.
You confirmSensitive or irreversible actions. One approval = one action.
EUServer 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.

  1. A third party receives your data. Expenses, amounts, suppliers, names, and receipts go to your AI provider. Mooncard does not control it.
  2. Its terms apply. Check its privacy, retention, and training terms, and your company policy, before you connect.
  3. The connector can write. It creates, changes, and deletes real data. Some actions are not reversible from the assistant. Read what you approve.
  4. No money moves. No transfer, no payment, no card transaction.
  5. Assistants make mistakes. Check each figure before you use it.
  6. Not professional advice. Not accounting, tax, legal, or financial advice.
  7. Content can carry instructions. Text in titles, comments, or receipts can steer the assistant. Decline actions that you did not ask for.
  8. You own the access. Choose the scopes and who gets the credentials. Delete the credential to revoke all access.
  9. You can stop at any time. Remove the connector: your connection ends. Delete the credential: it ends for everyone.
  10. 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.

Base URLhttps://sandbox.mooncard.co · https://app.mooncard.co
Version/api/v3
AuthenticationAuthorization: Bearer <ACCESS_TOKEN>
FormatJSON. multipart/form-data to upload a file.

How to read an endpoint page

  • The method and the URL.
  • The parameters: path, query, and body.
  • A request example in cURL, Python, JavaScript, and Ruby.
  • The response codes, an example response, and the response fields.

How to use this reference

  1. Get startedRead Onboarding API and the developer guidelines.
  2. Get a tokenSee Access token.
  3. Find the endpointUse the menu or its search. Endpoints are grouped by domain.
  4. Try itCopy the example, or use Browse the API in the portal. See Browse the API in your browser.
  5. Handle errorsSee Call the API.

Good to know

  • Each request needs an access token. It acts for one user in one company, with the role of this user.
  • Lists use page and per_page (max 200). The total is in X-Total-Count.

Authentication

Mooncard uses OAuth 2. It allows two grant flows to get a token, and the refresh token flow to renew it.

Domains

Developer guidelines

From idea to production

7 steps. Each one links to its page.

#StepWhat to do
1DiscoverFind your endpoints and scopes. Full list: Browse the API.
2Sign upCreate your account. No manual review.
3Usage contextOwn 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.
4SandboxGenerate it, then sign in to sandbox.mooncard.co.
5First callSandbox credential → token → GET /api/v3/me.
6BuildSee the checklist below.
7ReleaseSee the checklist below and Go to production.

6. Build

  • Authorization code flow
  • Refresh tokens, no new sign-in
  • per_page=200 + X-Total-Count
  • Webhooks, not polling
  • Handle 401 403 422
  • Minimum scopes
  • Secrets on the server

7. Release

  • New Production credential
  • Correct usage context, clear description
  • HTTPS redirect URI
  • Status authorized
  • app.mooncard.co instead of sandbox.mooncard.co
  • Production client ID and secret
  • Password flow: the production company_id from your account manager

Developer Portal

The Developer Portal

developers.mooncard.co: your account, teammates, sandbox, and credentials.

New here? Create your account first.

Menu

MenuItemUse
OverviewThe getting-started guide, then your API and MCP flows
SandboxYour sandbox and its resource-owner credentials
CredentialsAPI credentialsCreate your credential
MCP credentialsCreate an MCP credential
DocumentationFull documentationThis guide
API referenceBrowse the API
MCP guideConnect an AI agent
Production guideGo to production
OAuth documentationOAuth reference
Your emailAccountYour information
Organization & teammatesShare your credentials

Work as a team

Invite teammates to your credentials, API and MCP. No more secrets sent by email or chat.

  1. Have a credentialAPI or MCP. Required before you invite.
  2. Organization & teammatesIn the menu with your email.
  3. Invite a teammateEnter their work email and select the credential. It also works before they have an account.
ActionOwnerTeammate
See credentials, use their client ID and secretYesYes
Edit a credentialYesNo
Rotate the secret, deleteYesNo
Invite teammatesYesNo

Someone leaves the project? Remove their access, then rotate the secret of each shared credential.