Create velocity control
POST /v1/api-gateway/controls/velocity
Creates a limit for exactly one of card_, cardholder_ or external_. 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_ is a whole count of at least 1). velocity_ 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_ webhook instead, so a limit can be sized before it is enforced). only_ restricts the card to an allowlist of merchants. enabled defaults to true.
Request
Authentication: your API key in the x-api-key header.
Headers
| Field | Type | Description |
|---|---|---|
Idempotency-Key | string | A 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
| Field | Type | Description |
|---|---|---|
limit_ required | number | The 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 required | string | Kind of control. SPEND caps the money spent in the window; COUNT caps the number of approved authorizations, and then limit_ is that whole count (an integer of at least 1).One of: SPEND, COUNT |
velocity_ required | string | Window 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 |
mode | string | What happens on a breach. STRICT (the default) declines the authorization; WATCH never declines and sends a VELOCITY_ webhook instead, so a limit can be sized before it is enforced.One of: STRICT, WATCH |
only_ | array of object | Restrict 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_ | string | Case-insensitive substring of the acquirer merchant name, e.g. "TRAIN TICKETS". Trimmed before matching. |
only_ | string | Exact four-digit merchant category code, e.g. "5734". |
card_ | string, uuid | Apply the control to one card. Exactly one of card_, cardholder_, external_ is required. |
cardholder_ | string, uuid | Apply the control to every card of this Moon cardholder (the cardholder id). |
external_ | string | Apply the control to every card of the cardholder identified by your own id (the cardholder external_). Canonical name for the value this route also accepts as end_. Send one or the other; sending both with different values is rejected. |
end_ | string | Deprecated alias of external_, kept working for existing integrations. Send one or the other; sending both with different values is rejected. |
enabled | boolean | Whether the control is active. Defaults to enabled. |
metadata | object | Your 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/v1/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
| Field | Type | Description |
|---|---|---|
id required | string | Velocity control id. |
card_ required | string, nullable | Card this control applies to, or null for cardholder-level controls. |
cardholder_ required | string, nullable | Cardholder this control applies to, or null for card-level controls. |
limit_ required | number | The 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 required | string | Control type: SPEND or COUNT. |
velocity_ required | string | Window the limit applies to: MONTH, DAY, WEEK or TOTAL, all UTC. |
mode required | string | STRICT declines a breach; WATCH only reports it over the webhook. |
only_ required | array of object | Merchant allowlist, or null when the card is not locked to any merchant. |
only_ | string | |
only_ | string | |
fixed required | integer | 1 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_). 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 required | integer | 1 when enabled, otherwise 0.One of: 0, 1 |
metadata required | object | The 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_ required | string | |
updated_ required | string |
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
Card, cardholder or organization not found (CARD_, CARDHOLDER_, ORGANIZATION_).
The body is the standard error object: see Errors.
409 Error
The Idempotency-Key you sent is already in use. IDEMPOTENCY_ — the key was used for a different request; send a new key, or repeat the original request exactly to receive its stored response. IDEMPOTENCY_ — 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_.
The body is the standard error object: see Errors.
Last updated August 16, 2026