Browse the docs

Create velocity control

POST /v2/api-gateway/controls/velocity

Creates a limit for exactly one of card_id, cardholder_id or external_id (your cardholder identifier). type is SPEND (a cap on the money spent in the window, in the card currency) or COUNT (a cap on how many authorizations are approved in it, so limit_amount is a whole count of at least 1). velocity_window is MONTH, DAY, WEEK or TOTAL (the lifetime of the card or cardholder), all UTC. mode is STRICT (the default, declines a breach) or WATCH (never declines; sends a VELOCITY_CONTROL_WATCHED webhook instead, so a limit can be sized before it is enforced). only_merchants restricts the card to an allowlist of merchants. enabled defaults to true. Creating a second control for the same target is allowed (no 409).

Request

Authentication: your API key in the x-api-key header.

Headers

FieldTypeDescription
Idempotency-KeystringA unique string you choose, up to 255 characters, that makes this request safe to retry. If a request times out or fails in transit, send it again with the same key: the status code and body of the first attempt are returned and the operation is not performed a second time. Stored replies — including failures — are replayed for 24 hours, after which the same key starts a fresh request. Reusing a key for a different request, or sending it while the first request is still running, returns 409. Omit the header and the request behaves exactly as before. Document upload endpoints do not accept it.

Body

FieldTypeDescription
limit_amount requirednumberThe cap for the window. On a SPEND control it is an amount in major units of the card's own currency — the currency of the ledger account the card draws on, which is what the authorization engine compares (e.g. 500). On a COUNT control it is a whole number of approved authorizations (an integer of at least 1).
type requiredstringKind of control. SPEND caps the money spent in the window; COUNT caps the number of approved authorizations, and then limit_amount is that whole count (an integer of at least 1).One of: SPEND, COUNT
velocity_window requiredstringWindow the cap applies to, always UTC. DAY is the calendar day, WEEK the ISO week starting Monday, MONTH the calendar month, TOTAL the lifetime of the card or cardholder.One of: MONTH, DAY, WEEK, TOTAL
modestringWhat happens on a breach. STRICT (the default) declines the authorization; WATCH never declines and sends a VELOCITY_CONTROL_WATCHED webhook instead, so a limit can be sized before it is enforced.One of: STRICT, WATCH
only_merchantsarray of objectRestrict the card to these merchants. An authorization whose merchant matches none of the entries is declined (or reported, in WATCH mode). Within one entry every field given must match; any entry matching is enough. name is matched case-insensitively as a substring of the acquirer descriptor, mcc exactly. Omit or send [] for no restriction. At most 50 entries.
only_merchants[].namestringCase-insensitive substring of the acquirer merchant name, e.g. "TRAIN TICKETS". Trimmed before matching.
only_merchants[].mccstringExact four-digit merchant category code, e.g. "5734".
card_idstring, uuidApply the control to one card. Exactly one of card_id, cardholder_id, external_id is required.
cardholder_idstring, uuidApply the control to every card of this Moon cardholder (the cardholder id).
external_idstringApply the control to every card of the cardholder identified by your own id (the cardholder external_id). Canonical name for the value this route also accepts as end_customer_id. Send one or the other; sending both with different values is rejected.
end_customer_idstringDeprecated alias of external_id, kept working for existing integrations. Send one or the other; sending both with different values is rejected.
enabledbooleanWhether the control is active. Defaults to enabled.
metadataobjectYour own key-value data to store on this velocity control. It is returned verbatim on every read of the velocity control and never affects how the velocity control is processed — use it to carry your order or customer identifier so you do not have to keep a separate mapping. Up to 50 keys, keys up to 40 characters and values up to 500 characters. Values must be strings — nested objects, arrays, numbers and booleans are rejected. On update the map you send replaces the stored map in full: omit the field to leave it unchanged, send {} to clear it, and send a key with an empty string value to remove just that key.

Example request

cURL

curl --request POST \
     --url 'https://api.themooncard.com/v2/api-gateway/controls/velocity' \
     --header 'x-api-key: YOUR_API_KEY' \
     --header 'content-type: application/json' \
     --data '{
       "limit_amount": 500,
       "type": "SPEND",
       "velocity_window": "MONTH",
       "mode": "STRICT",
       "only_merchants": [
         {
           "name": "string",
           "mcc": "string"
         }
       ],
       "card_id": "8d2cd518-5ef2-4280-91e8-63b1a0e672e7",
       "cardholder_id": "8d2cd518-5ef2-4280-91e8-63b1a0e672e7",
       "external_id": "string",
       "end_customer_id": "string",
       "enabled": false,
       "metadata": {
         "team": "field-ops"
       }
     }'

Responses

201 Success
FieldTypeDescription
id requiredstringVelocity control id.
card_id requiredstring, nullableCard this control applies to, or null for cardholder-level controls.
cardholder_id requiredstring, nullableCardholder this control applies to, or null for card-level controls.
limit_amount requirednumberThe cap for the window: an amount in the card's own currency on a SPEND control, a whole number of approved authorizations on a COUNT one.
type requiredstringControl type: SPEND or COUNT.
velocity_window requiredstringWindow the limit applies to: MONTH, DAY, WEEK or TOTAL, all UTC.
mode requiredstringSTRICT declines a breach; WATCH only reports it over the webhook.
only_merchants requiredarray of objectMerchant allowlist, or null when the card is not locked to any merchant.
only_merchants[].namestring
only_merchants[].mccstring
fixed requiredinteger1 when Moon fixed this control to the card it belongs to: the control that makes a card single-use, or the one that locks it to a merchant. A fixed control is read like any other but cannot be updated or deleted here (CONTROL_IS_FIXED). What to do instead depends on which one it is: a merchant lock is changed or removed through the card merchant-lock endpoints, while a single-use limit cannot be lifted at all — it ends with the card, so terminate the card to stop it being used. Otherwise 0.One of: 0, 1
enabled requiredinteger1 when enabled, otherwise 0.One of: 0, 1
metadata requiredobjectThe key-value data you stored on this velocity control, returned exactly as you sent it. An empty object when you never set any — it is never null.
created_at requiredstring
updated_at requiredstring

JSON

{
  "id": "e2c4a6b8-0d1f-4a2b-8c3d-4e5f6a7b8c9d",
  "card_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "cardholder_id": null,
  "limit_amount": 500,
  "type": "SPEND",
  "velocity_window": "MONTH",
  "mode": "STRICT",
  "only_merchants": null,
  "fixed": 0,
  "enabled": 1,
  "metadata": {
    "team": "field-ops"
  },
  "created_at": "2026-08-01T09:12:44.000Z",
  "updated_at": "2026-08-16T17:51:01.000Z"
}
400 Error

Validation failed (errors[] lists each offending field) or a domain rule rejected the request ({ message, code }).

The body is the standard error object: see Errors.

401 Error

Missing or invalid credentials, e.g. { "message": "Invalid API Key", "code": "UNAUTHORIZED" }.

The body is the standard error object: see Errors.

404 Error

The referenced card (CARD_NOT_FOUND), cardholder (CARDHOLDER_NOT_FOUND) or your top-level organization (ORGANIZATION_NOT_FOUND) does not exist or is not owned by this organization.

The body is the standard error object: see Errors.

409 Error

The Idempotency-Key you sent is already in use. IDEMPOTENCY_KEY_REUSED — the key was used for a different request; send a new key, or repeat the original request exactly to receive its stored response. IDEMPOTENCY_KEY_IN_PROGRESS — the first request with this key has not finished yet; retry in a few seconds.

The body is the standard error object: see Errors.

500 Error

Unexpected server error. Body is { message: "Internal server error", code: "INTERNAL_SERVER_ERROR" }.

The body is the standard error object: see Errors.

Last updated August 24, 2026