Managing a Card
Activate, block, unblock, close, rename a card and change its spending limits.
Before You Start
Read the following guides before proceeding:
| Guide | Why |
|---|---|
| Getting Started | Platform overview and setup |
| Api Basics | Required headers and request configuration |
| Authentication | How to obtain the corporation token |
| Issuing a Card | Where {cardId} comes from |
| Card Details | Reading allowed_actions before acting |
Overview
Card management is status-driven. Every action is evaluated against the card's current status before it
runs, and the same evaluator produces the allowed_actions array on each card in
GET /api/v1/cards. Read allowed_actions and act on what it lists — it is the API's own answer to
what is possible right now.
Status Lifecycle
Requested → NotActivated → Active
↓
Blocked ←→ Active
↓
Closed
| Status | Description |
|---|---|
Requested | The order is with the processor. Nothing can be done to the card yet |
NotActivated | Issued and awaiting activation. Plastic cards land here |
Active | Usable |
Blocked | Temporarily suspended. Reversible |
Closed | Permanently closed. Terminal |
A virtual card goes straight from Requested to Active.
Action Gates
| Action | Allowed in | Refused in |
|---|---|---|
| Activate | NotActivated | Requested (Card is not issued yet), Active, Blocked, Closed (Card is already activated) |
| Block | Active | Requested, NotActivated, Closed (Card status does not allow blocking), Blocked (Card is already blocked) |
| Unblock | Blocked | Everything else (Card is not blocked) |
| Close | Active, Blocked | Requested, NotActivated (Card status does not allow closing), Closed (Card is already closed) |
| SetLimit | Active | Everything else (Card status does not allow limit changes) |
| GetDetails, GetCvv, GetPin | Active | Everything else |
Renaming a card is the one operation with no status gate — a card in any status can be renamed.
Activate a Card
PUT /api/v1/cards/{cardId}/activate
Requires CardSu or CardManage. Only applies to physical cards; the two body fields prove the caller
is holding the card that arrived.
Path parameters:
| Parameter | Description |
|---|---|
{cardId} | The card's id from GET /api/v1/cards |
Request body:
{
"card_number_last4": "5678",
"expiry_date": "01/2028"
}| Field | Type | Required | Description |
|---|---|---|---|
card_number_last4 | string | Yes | Last 4 digits of the printed card number. Must match ^\d{4}$ |
expiry_date | string | Yes | Expiry date printed on the card, in MM/YYYY. Must match ^(0[1-9]|1[012])/\d{4}$ |
Response:
{}The field is
card_number_last4— no underscore before the4— while the card record reports the
same value ascard_data.card_number_last_4. The two spellings are not interchangeable.
expiry_dateis four-digit year,01/2028, not01/28. A two-digit year fails the pattern.
Block and Unblock
PUT /api/v1/cards/{cardId}/block
PUT /api/v1/cards/{cardId}/unblock
Requires CardSu or CardManage. Neither takes a body.
Response:
{}Blocking stops all transactions on the card and is reversible. Use it for a lost card before a
replacement is decided; use close when the card is gone for good.
Close a Card
PUT /api/v1/cards/{cardId}/close
Requires CardSu or CardManage. Takes no body.
Response:
{}Closing is permanent. There is no reopen endpoint, and a
Closedcard cannot be blocked, unblocked,
re-limited or read. Closing is also what frees a slot against the issuance ceiling — 30 virtual and
15 plastic cards per corporation.
Rename a Card
PUT /api/v1/cards/{cardId}/name
Requires CardSu or CardManage.
Request body:
{
"name": "Marketing team card"
}| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | New display name. Must match ^[a-zA-Z0-9]+[a-zA-Z0-9\-':+#& ]{1,50}$ |
Response:
{}This changes card_data.card_name only — the display label. It does not change name_on_card, which is
embossed at issuance and cannot be changed afterwards.
Change Spending Limits
PUT /api/v1/cards/{cardId}/limit
Requires CardSu or CardLimitManage. The card must be Active.
Request body:
{
"daily_limit": 1000.00,
"monthly_limit": 10000.00,
"transaction_limit": 500.00
}| Field | Type | Required | Description |
|---|---|---|---|
daily_limit | number | No | Maximum spend per day. -1 disables it |
monthly_limit | number | No | Maximum spend per calendar month. -1 disables it |
transaction_limit | number | No | Maximum per single transaction. -1 disables it |
limit | number | No | Obsolete alias for daily_limit. When both are sent, limit wins |
Response:
{}This call replaces the whole limit set, it does not patch it. Any of the three limits you omit is
set to-1— disabled. Sending onlydaily_limitsilently removes the monthly and per-transaction
limits. Always read the current limits fromGET /api/v1/cardsand send all three values back,
changing the one you mean to change.
The lifetime limit is set to
-1on every call and cannot be configured through this API.
Reading Current Limits
{
"limit": {
"daily_limit": 1000.0,
"daily_usage": 200.0,
"monthly_limit": 10000.0,
"monthly_usage": 2000.0,
"transaction_limit": 500.0,
"currency": "USD"
}
}| Field | Description |
|---|---|
daily_limit / monthly_limit | Configured ceilings. -1 means no limit |
daily_usage / monthly_usage | Spent so far in the current period |
transaction_limit | Per-transaction ceiling |
currency | ISO 4217 currency the limits and usage are expressed in |
Limit and usage changes are also delivered to
POST {your_webhook_base_url}/v2/webhooks/card-limits.
Code
// Read current limits first — omitted limits are disabled, not preserved
const cardsResponse = await fetch(`${baseUrl}/api/v1/cards`, {
headers: { 'Authorization': `Bearer ${corporationToken}` }
});
const { data: cards } = await cardsResponse.json();
const current = cards.find((c) => c.id === cardId).limit;
await fetch(`${baseUrl}/api/v1/cards/${cardId}/limit`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${corporationToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
daily_limit: 2000.00,
monthly_limit: current.monthly_limit,
transaction_limit: current.transaction_limit
})
});# Read current limits first — omitted limits are disabled, not preserved
cards = requests.get(
f"{base_url}/api/v1/cards",
headers={"Authorization": f"Bearer {corporation_token}"},
).json()["data"]
current = next(c for c in cards if c["id"] == card_id)["limit"]
requests.put(
f"{base_url}/api/v1/cards/{card_id}/limit",
headers={
"Authorization": f"Bearer {corporation_token}",
"Content-Type": "application/json",
},
json={
"daily_limit": 2000.00,
"monthly_limit": current["monthly_limit"],
"transaction_limit": current["transaction_limit"],
},
)// Read current limits first — omitted limits are disabled, not preserved
listReq, _ := http.NewRequest("GET", baseURL+"/api/v1/cards", nil)
listReq.Header.Set("Authorization", "Bearer "+corporationToken)
listResp, _ := http.DefaultClient.Do(listReq)
defer listResp.Body.Close()
var cardsResp struct {
Data []CardResponse `json:"data"`
}
json.NewDecoder(listResp.Body).Decode(&cardsResp)
current := findCard(cardsResp.Data, cardId).Limit
body, _ := json.Marshal(map[string]float64{
"daily_limit": 2000.00,
"monthly_limit": current.MonthlyLimit,
"transaction_limit": current.TransactionLimit,
})
req, _ := http.NewRequest("PUT", baseURL+"/api/v1/cards/"+cardId+"/limit", bytes.NewBuffer(body))
req.Header.Set("Authorization", "Bearer "+corporationToken)
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()Webhooks
Card Webhook
Endpoint: POST {your_webhook_base_url}/v2/webhooks/cards
Delivered on every card state change — issuance, activation, block, unblock, close.
The payload is the shared card shape and carries fields the Corporate API's own card list does not:
generation, balances, and lifetime_limit / lifetime_usage inside limit. Ignore them.
Card Limit Webhook
Endpoint: POST {your_webhook_base_url}/v2/webhooks/card-limits
Delivered when a limit or its usage changes.
See Webhooks for the delivery contract.
Error Handling
{
"error_reason": "ErrorInvalidStatus",
"error_description": "Action is not allowed",
"error_category": {
"category": "CategoryValidationFailure",
"http_status_code": 400
},
"error_details": [
{ "key": "action_type", "details": "Block" },
{ "key": "value", "details": "Blocked" }
]
}The action_type detail names the action and value names the card status that refused it.
Validation Errors (400)
| Error Reason | Description | Resolution |
|---|---|---|
ErrorInvalidStatus | Card is not issued yet | The card is Requested. Wait for the issuance webhook |
ErrorInvalidStatus | Card is already activated | The card is past NotActivated |
ErrorInvalidStatus | Card status does not allow blocking | Only an Active card can be blocked |
ErrorInvalidStatus | Card is already blocked | Read allowed_actions before acting |
ErrorInvalidStatus | Card is not blocked | Unblock applies only to a Blocked card |
ErrorInvalidStatus | Card status does not allow closing | A Requested or NotActivated card cannot be closed |
ErrorInvalidStatus | Card is already closed | Terminal state |
ErrorInvalidStatus | Card status does not allow limit changes | The card must be Active |
ErrorMissingField | card number last4 is required | Send the last 4 printed digits |
ErrorInvalidField | Invalid format for card number last4 | Exactly 4 digits |
ErrorMissingField | expiry date is required | Send the printed expiry date |
ErrorInvalidField | Invalid format for expiry date | MM/YYYY with a four-digit year |
ErrorMissingField | name is required | Send a display name |
ErrorInvalidField | Invalid format for name | Match the card-name pattern |
Permission Errors (403)
| Error Reason | Description | Resolution |
|---|---|---|
ErrorPermissionDenied | User does not have required permissions | Status changes need CardManage; limits need CardLimitManage |
Server Errors (500)
| Error Reason | Description | Resolution |
|---|---|---|
ErrorGeneral | Failed to update card limit | The limit service rejected the change. Retry |
ErrorGeneral | Card lookup failure | The {cardId} does not belong to this corporation |
Updated 20 days ago

