Issuing a Card
Order virtual and plastic corporate cards, settle the issuance fee, and link the card to an employee.
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 |
| Capabilities | Card issuance is capability-gated |
| Employees | A card is linked to an employee record |
Overview
A corporate card is issued against the corporation wallet and optionally linked to an employee. Two
formats are available — Virtual and Plastic. The Corporate API does not issue metal cards.
Whether an issuance fee applies depends on your company configuration. When fees are enforced, the fee
must be settled before the card is created, and there are two flows for doing that.
Prerequisites
Read GET /api/v1/corporations and confirm the capability for the format you want is Active:
| Format | Capability | Ceiling |
|---|---|---|
| Virtual | VisaVirtualCard | 30 virtual cards per corporation |
| Plastic | VisaPlasticCard | 15 plastic cards per corporation |
Both also require the corporation profile to be Active. At the ceiling the capability flips to
NotAvailable with You have reached the maximum number of virtual cards allowed — close a card before
issuing another.
Choosing a Fee Flow
GET /api/v1/cards/{type}/fees/{country}
Call this first. Its answer decides the whole flow.
| Response | Meaning | What to do |
|---|---|---|
400 ErrorNotSupported — You have no card order/delivery fees to pay | Fees are not enforced for your company | Skip every fee step. Issue the card directly |
400 ErrorNotSupported — Card issuance is not supported for this company | No card configuration exists for your company | Contact Wirex. No issuance flow will work |
200 with order_fee | Fees are enforced | Pick v1 or v2 below |
When fees are enforced, two flows reach the same card:
| v1 — External fee | v2 — Direct fee | |
|---|---|---|
| How the fee is collected | You transfer it on-chain yourself | Debited by Wirex at issuance |
| Fee invoice endpoint | POST /api/v1/cards/{type}/fees/{country}/payment | POST /api/v2/cards/{type}/fees/{country}/payment |
| Fee invoice returns | recipient_address — where to send the fee | delivery_id — plus recipient_address |
| Field passed at issuance | payment_transaction_hash | delivery_id |
| Issuance endpoint | POST /api/v1/cards/virtual, POST /api/v1/cards/plastic | POST /api/v2/cards/virtual, POST /api/v2/cards/plastic |
| Failure mode | You control the transfer; a wrong amount is your reconciliation problem | The debit is atomic with issuance |
Pick one and follow it end to end. Do not mix a v2 invoice with a v1 issuance call — delivery_id is
not read by the v1 endpoints, and payment_transaction_hash is not read by the v2 endpoints.
Shared Lookups
Check Order Fees
GET /api/v1/cards/{type}/fees/{country}
Requires CardSu, CardCreate or CardView.
Path parameters:
| Parameter | Description |
|---|---|
{type} | Plastic, Virtual or Metal. Case-insensitive — virtual and VIRTUAL both resolve |
{country} | Country the card is issued in, as an uppercase ISO 3166-1 alpha-2 code, e.g. GB |
Response:
{
"currency": "USD",
"order_fee": 9.99,
"estimated_payment_amounts": [
{
"amount": 9.99,
"precise_amount": "9990000000000000000",
"token_address": "0x0774164DC20524Bb239b39D1DC42573C3E4C6976",
"token_symbol": "WUSD",
"rate": 1.0,
"fee_amount": 0,
"fee_precise_amount": "0"
}
]
}| Field | Description |
|---|---|
currency | Fiat currency the fee is denominated in. A fee configured in USDC is reported as USD |
order_fee | Order fee in currency. Excludes delivery — see below |
estimated_payment_amounts[] | The fee converted into each supported stablecoin |
estimated_payment_amounts[].precise_amount | Amount in the token's smallest unit, as a string |
estimated_payment_amounts[].rate | Rate applied for that token |
order_feeis the card order fee only. For a plastic card the delivery fee is added on top, and the
invoice endpoint is what returns the combined total. Do not presentorder_feeas the amount the
corporation will pay for a physical card.
List Delivery Countries
GET /api/v1/cards/delivery/countries
Requires CardSu, CardCreate or CardView. Returns the countries a plastic card can be delivered to.
Response:
["GB", "DE", "FR", "ES", "IT"]List Delivery Methods
GET /api/v1/cards/delivery/methods/{country}
Requires CardSu, CardCreate or CardView.
Path parameters:
| Parameter | Description |
|---|---|
{country} | Delivery country, ISO 3166-1 alpha-2 |
Response:
[
{
"provider": "DHL",
"fee": 15.0,
"currency": "USD",
"estimated_payment_amounts": [
{
"amount": 15.0,
"precise_amount": "15000000000000000000",
"token_address": "0x0774164DC20524Bb239b39D1DC42573C3E4C6976",
"token_symbol": "WUSD",
"rate": 1.0
}
]
}
]| Field | Description |
|---|---|
provider | Courier identifier. This is the value passed as delivery_provider |
fee | Delivery fee in currency |
estimated_payment_amounts[] | The delivery fee converted into each supported stablecoin |
Flow v1: External Fee
Step 1: Create the Fee Invoice
POST /api/v1/cards/{type}/fees/{country}/payment
Requires CardSu or CardCreate.
Request body:
{
"token_address": "0x0774164DC20524Bb239b39D1DC42573C3E4C6976",
"delivery_provider": "DHL"
}| Field | Type | Required | Description |
|---|---|---|---|
token_address | string | Yes | Token used to pay the fee. Must exist in GET /api/v1/config/tokens |
delivery_provider | string | Conditional | Required when {type} is not Virtual; omit for virtual cards |
Response:
{
"recipient_address": "0xAAFF0821A09A1Aac28B72dD3Ff410A7ea5FEb874",
"payment_amount": {
"amount": 24.99,
"precise_amount": "24990000000000000000",
"token_address": "0x0774164DC20524Bb239b39D1DC42573C3E4C6976",
"token_symbol": "WUSD",
"rate": 1.0
}
}| Field | Description |
|---|---|
recipient_address | Address to transfer the fee to |
payment_amount | The total to transfer — order fee plus delivery fee, converted to token_address |
Step 2: Pay the Fee On-Chain
Transfer payment_amount.precise_amount of payment_amount.token_address to recipient_address from
the corporation wallet, and keep the transaction hash. Step 3 consumes it as
payment_transaction_hash.
Step 3: Issue the Card
POST /api/v1/cards/virtual
POST /api/v1/cards/plastic
Requires CardSu or CardCreate.
Request body — virtual:
{
"card_name": "Marketing team card",
"name_on_card": "Acme Inc IT",
"employee_id": "64120850-73a1-4df5-a074-d463258c9deb",
"payment_transaction_hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
}Request body — plastic:
{
"card_name": "Marketing team card",
"name_on_card": "Acme Inc IT",
"employee_id": "64120850-73a1-4df5-a074-d463258c9deb",
"delivery_provider": "DHL",
"payment_transaction_hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"delivery_address": {
"line1": "10 Downing Street",
"line2": "Flat 2",
"city": "London",
"state": "",
"postal_code": "SW1A 2AA",
"country": "GB"
}
}| Field | Type | Required | Description |
|---|---|---|---|
card_name | string | No | Display name shown in the card list |
name_on_card | string | No | Name embossed on the card. Overridden when the linked employee has complete personal data |
employee_id | string | No | Employee to link the card to. Must belong to this corporation |
payment_transaction_hash | string | Conditional | Required when fees are enforced; omit otherwise |
delivery_provider | string | Conditional | Plastic only. Required when fees are enforced. Defaults to DHL when fees are not enforced |
delivery_address | object | Yes for plastic | Where the card is shipped |
delivery_address.line1 | string | Yes | House number and street |
delivery_address.line2 | string | No | Apartment, suite, floor |
delivery_address.city | string | Yes | City |
delivery_address.state | string | No | State or province |
delivery_address.postal_code | string | Yes | Postal code |
delivery_address.country | string | Yes | Uppercase ISO 3166-1 alpha-2 |
Response:
{
"id": "64120850-73a1-4df5-a074-d463258c9deb"
}| Field | Description |
|---|---|
id | The card id. Every card endpoint takes it as {cardId} |
Flow v2: Direct Fee
Step 1: Create the Fee Invoice
POST /api/v2/cards/{type}/fees/{country}/payment
Requires CardSu or CardCreate. The request body is identical to v1.
Response:
{
"delivery_id": 12345,
"recipient_address": "0xAAFF0821A09A1Aac28B72dD3Ff410A7ea5FEb874",
"payment_amount": {
"amount": 24.99,
"precise_amount": "24990000000000000000",
"token_address": "0x0774164DC20524Bb239b39D1DC42573C3E4C6976",
"token_symbol": "WUSD",
"rate": 1.0
}
}| Field | Description |
|---|---|
delivery_id | Integer reference for the issuance call. Single-use |
recipient_address | The platform Buffer contract address the fee settles into |
payment_amount | Order fee plus delivery fee, converted to token_address |
delivery_idis consumed by the issuance call, not by the invoice call. A failed issuance
attempt that got past validation may leave it spent. Do not retry a failed v2 issuance with the same
delivery_id— create a new invoice.
Step 2: Issue the Card
POST /api/v2/cards/virtual
POST /api/v2/cards/plastic
Requires CardSu or CardCreate.
Request body — virtual:
{
"card_name": "Marketing team card",
"name_on_card": "Acme Inc IT",
"employee_id": "64120850-73a1-4df5-a074-d463258c9deb",
"delivery_id": 12345
}Request body — plastic:
{
"card_name": "Marketing team card",
"name_on_card": "Acme Inc IT",
"employee_id": "64120850-73a1-4df5-a074-d463258c9deb",
"delivery_provider": "DHL",
"delivery_id": 12345,
"delivery_address": {
"line1": "10 Downing Street",
"line2": "Flat 2",
"city": "London",
"state": "",
"postal_code": "SW1A 2AA",
"country": "GB"
}
}| Field | Type | Required | Description |
|---|---|---|---|
delivery_id | integer | Conditional | Required when fees are enforced, and must be positive. Ignored when fees are not enforced |
| Other fields | As in v1, except that payment_transaction_hash is not used |
Response:
{
"id": "64120850-73a1-4df5-a074-d463258c9deb"
}Code
const response = await fetch(`${baseUrl}/api/v2/cards/virtual`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${corporationToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
card_name: 'Marketing team card',
employee_id: employeeId,
delivery_id: deliveryId // omit if fees not enforced
})
});
const { id: cardId } = await response.json();response = requests.post(
f"{base_url}/api/v2/cards/virtual",
headers={
"Authorization": f"Bearer {corporation_token}",
"Content-Type": "application/json",
},
json={
"card_name": "Marketing team card",
"employee_id": employee_id,
"delivery_id": delivery_id, # omit if fees not enforced
},
)
card_id = response.json()["id"]body, _ := json.Marshal(map[string]interface{}{
"card_name": "Marketing team card",
"employee_id": employeeID,
"delivery_id": deliveryID, // omit if fees not enforced
})
req, _ := http.NewRequest("POST", baseURL+"/api/v2/cards/virtual", 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()
var issueResp struct {
Id string `json:"id"`
}
json.NewDecoder(resp.Body).Decode(&issueResp)Linking a Card to an Employee
employee_id is optional. It has three consequences, and all three are decided at issuance:
- Visibility. An employee without
SuorCardSusees only the cards linked to them. An
unlinked card is invisible to everyone but a card administrator. - The name on the card. The embossed name becomes the employee's
"<first_name> <last_name>", truncated to 25 characters, and thename_on_cardfield in the
request is ignored. This applies whenever the employee record carriesfirst_name,
last_name,emailandphone— all four are required at invite, so it applies to every
employee.name_on_cardis used as sent only when noemployee_idis linked. - The billing address. With a complete employee record, the cardholder payload also carries the
corporation's legal address as the billing address. A corporation with no legal address on record
fails issuance with400 ErrorMissingFieldand
Failed to determine billing address for the card.
The employee is validated before the fee is charged, so a foreign or unknown employee_id fails
without spending delivery_id. An employee_id from another corporation returns 401 with
Employee does not belong to corporation.
To emboss a corporate name such as
Acme Inc IT, leaveemployee_idunset. A linked employee
always overridesname_on_card, and the card is then invisible to everyone withoutCardSu— that
is the trade-off, and it cannot be changed after issuance.
Field Validation
| Field | Required | Validation (regex) | Notes |
|---|---|---|---|
card_name | No | ^[a-zA-Z0-9]+[a-zA-Z0-9\-':+#& ]{1,50}$ | Must start alphanumeric; 2–51 characters overall |
name_on_card | No | ^[A-Za-z][A-Za-z .'-]{1,25}$ | Starts with a letter; 2–26 characters. No digits |
delivery_address.line1, .line2 | Yes / No | ^[A-Za-z0-9&.,'’\-/() :+#]{2,100}$ | 2–100 characters |
delivery_address.city | Yes | ^[A-Za-z\s\-'.]{2,100}$ | Letters only — a city containing a digit is rejected |
delivery_address.state | No | ^[A-Za-z][A-Za-z\s\-']{1,99}$ | Letters only |
delivery_address.postal_code | Yes | ^[A-Za-z0-9\s\-]{3,12}$ | 3–12 characters |
delivery_address.country | Yes | ^[A-Z]{2}$ | Uppercase ISO 3166-1 alpha-2 |
postal_codeis always required, including for countries that have no postal code. Send a
placeholder that matches the pattern, e.g."00000". Only the format is validated, not real postal
validity.
The field is
postal_codeon the way in andzip_codeon the way out. The issuance request takes
delivery_address.postal_code; the card returned byGET /api/v1/cardsreports the same value as
delivery_address.zip_code. Map between them explicitly.
Address validation is format-only. An address that passes here can still be rejected by the card
processor, in which case the issuance call returns a synchronous 400 — no card is created and no card
webhook is sent.
What Happens Next
- The card is created in status
Requested. - A virtual card moves to
Activeonce the processor completes issuance. A plastic card moves to
NotActivatedand must be activated on arrival — see Managing a Card. - Each transition is delivered to
POST {your_webhook_base_url}/v2/webhooks/cards. card_data.card_number_first_4is present while the card isRequestedorNotActivated;
card_number_last_4andexpiry_dateappear once it is past those states.
Error Handling
{
"error_reason": "ErrorNotSupported",
"error_description": "You have no card order/delivery fees to pay",
"error_category": {
"category": "CategoryValidationFailure",
"http_status_code": 400
},
"error_details": [
{ "key": "field", "details": "fees_enforced" }
]
}Validation Errors (400)
| Error Reason | Error Details | Description | Resolution |
|---|---|---|---|
ErrorNotSupported | field: fees_enforced | You have no card order/delivery fees to pay | Fees are not enforced. Skip the fee endpoints and issue directly |
ErrorNotSupported | field: company_id | Card issuance is not supported for this company | No card configuration exists for your company. Contact Wirex |
ErrorNotFound | field: type, field: country | No fees found for specified country and method | The format is not offered in that country |
ErrorMissingField | field: delivery_provider | delivery provider is required for physical cards | Send a provider value from GET /api/v1/cards/delivery/methods/{country} |
ErrorInvalidField | field: type | Invalid value for type | Use Plastic, Virtual or Metal |
ErrorInvalidField | field: country | Invalid format for country | Uppercase two-letter code |
ErrorMissingField | field: delivery_address.line1 | Delivery address incomplete | Send every required address field |
ErrorInvalidField | field: delivery_address.city | City contains disallowed characters | Letters, spaces, hyphens, apostrophes and periods only |
ErrorGeneral | delivery_id | Delivery ID is required and must be positive | Create a v2 invoice and pass its delivery_id |
ErrorGeneral | card_name | Cards name is invalid | Match the card_name pattern |
ErrorGeneral | name_on_card | Name on card is invalid | Match the name_on_card pattern |
ErrorMissingField | — | Failed to determine billing address for the card | The corporation has no legal address. Complete KYB |
ErrorGeneral | — | Capability is not active | VisaVirtualCard or VisaPlasticCard is not Active |
Authorization Errors (401, 403)
| HTTP | Error Reason | Description | Resolution |
|---|---|---|---|
| 401 | ErrorGeneral | Employee does not belong to corporation | The employee_id belongs to another corporation |
| 403 | ErrorPermissionDenied | User does not have required permissions | Issuance requires CardSu or CardCreate |
Server Errors (500)
| Error Reason | Description | Resolution |
|---|---|---|
ErrorNotFound | Order fees not found | No fee configuration for that format and country |
ErrorNotFound | Delivery fees not found | No delivery configuration for that country and provider |
ErrorGeneral | Requested payment token not found | token_address is not in the catalogue |
ErrorGeneral | Rate for requested payment token not found | No rate for the fee currency against that token. Choose another token |
ErrorGeneral | Failed to process delivery payment | The delivery_id was rejected — expired, already used, or unfunded. Create a new invoice |
ErrorGeneral | Failed to issue virtual card / Failed to issue plastic card | The processor rejected the order. No card was created and no webhook is sent |
ErrorGeneral | Failed to link card to employee | The card was issued but not linked. Read GET /api/v1/cards as a card administrator to find it |
ErrorConfigurationInvalid | Card configuration is missing | Platform configuration error. Contact Wirex |
Failed to link card to employeemeans the card exists. It is not visible to the employee and
cannot be re-linked through the API — there is no link endpoint. Retrying issuance creates a second
card.
Updated 20 days ago

