# Cards

A Jiko debit card is issued against a **pocket**. The card spends the pocket's balance, and because that balance is held in T-Bills rather than idle cash, every authorization is settled against the pocket's liquidation value, not a cash figure you can read directly.

div
## Prerequisites

### Feature flags

No feature flag gates cards in the Partner API. Enablement is commercial: contact your Jiko point of contact to have the card program turned on for your partnership. Until it is, every card endpoint is unreachable.

### Everything else

- **An approved customer and a pocket in `OPEN` status.** The card is issued against the pocket and spends its balance, so a `PENDING`, `FROZEN` or `CLOSED` pocket has nothing to issue against.
- **A fresh 4096-bit RSA keypair per call that returns card numbers.** PEM-encoded, PKCS#1 or PKCS#1.5. Each key may be used once — one that has already been spent is rejected — so generate a new pair for every create and every fetch, and keep the private key where you can decrypt with it.
- **Somewhere to decrypt.** The PAN, CVV and expiration only ever arrive encrypted. An integration that cannot hold a private key and decrypt server-side cannot read card numbers at all.
- **A shipping address, for a physical card**, and patience for the order to reach `CARD_CREATED` — `card_id` is `null` until then, so reading it immediately after ordering gets you nothing.
- **Jiko's `set_pin` public key, and a PIN step in your UX.** Fetch the key from `GET /api/v1/public-keys/`; the cardholder's PIN is encrypted to it and never travels in the clear. A physical card is not usable until the PIN is set — that is what activates it.
- **Balance that can actually move.** Authorizations settle against the pocket's liquidation value, not a cash figure. Read `liquidation_value_t0` and `liquidation_value_t1` on the portfolio rather than `total_value`.
- **A webhook subscription** for `card.status.*` and `card.transaction.*`. Subscriptions are recorded against the API user that creates them, so subscribe with the user whose traffic you want to hear about.
- **`POST /api/v1/sandbox/generate-card/`, to test a physical card.** Nothing is manufactured in sandbox, so without it an order never reaches a card you can transact on.


## Card types

Two card types:

| Type | How it is issued | How the numbers reach you |
|  --- | --- | --- |
| `VIRTUAL` | Created instantly against a pocket | Returned encrypted in the create response |
| `PHYSICAL` | Ordered, manufactured, shipped | Fetched encrypted once the card exists |


## Card numbers are never returned in the clear

You supply a **single-use 4096-bit PEM-encoded RSA public key** (PKCS#1 or PKCS#1.5) on the request, and Jiko returns the PAN, CVV and expiration encrypted to it. You decrypt with the matching private key on your side.

Each key may be used once. A key that has already been spent is rejected, so generate a fresh keypair per call.

```
POST /api/v1/jiko-accounts/{account_id}/virtual-cards/
```

```json
{ "public_key": "-----BEGIN PUBLIC KEY-----\n…" }
```

returns

```json
{
  "card_id": "…",
  "encrypted_pan": "…",
  "encrypted_cvv": "…",
  "encrypted_expiration": "…"
}
```

**Reading an existing card's numbers is also a `POST`**, on both card types, because the public key has to go in a request body:

| Card | Endpoint |
|  --- | --- |
| Virtual | `POST /api/v1/jiko-accounts/{account_id}/virtual-cards/{card_id}/` |
| Physical | `POST /api/v1/jiko-accounts/{account_id}/physical-cards/{card_id}/` |


Each takes the same `{ "public_key": … }` body and returns the same encrypted triple. A `GET` will not work.

### End to end

```bash
public_key="public_key.pem"
private_key="private_key.pem"
openssl genrsa -out $private_key 4096
openssl rsa -in $private_key -pubout -out $public_key

body=$(jq --null-input -c -r --arg public_key_body "$(<public_key.pem)" '{public_key: $public_key_body}')
response=$(send_jiko_request "POST" "/api/v1/jiko-accounts/$account_id/virtual-cards/" $body)

fields=("pan" "cvv" "expiration")
for field in "${fields[@]}"; do
   echo $response | jq -r ".encrypted_$field" | base64 -d > .temp_encrypted_data
   echo $field = $(openssl rsautl -decrypt -inkey private_key.pem -in .temp_encrypted_data)
done

rm .temp_encrypted_data $public_key $private_key
```

The same script reads an existing card back — change the URL to the card's own path and leave everything else alone. Generate the keypair on the device that will decrypt, and discard it afterward: Jiko monitors submitted public keys for duplicates, and a key that has already been used is refused.

## Ordering a physical card

```
POST /api/v1/jiko-accounts/{account_id}/card-orders/
```

| Field | Notes |
|  --- | --- |
| `name_on_card` | Up to 100 characters |
| `shipping_address` | `street_address` (≤100), `street_address2` (≤100, optional), `city` (≤100), `state`, `postal_code` (≤10) |


An order moves through its own statuses, separate from the card's:

| Order status | Meaning |
|  --- | --- |
| `INITIAL` | Order accepted, not yet processed |
| `CARD_CREATED` | A card record exists; `card_id` is now populated. Short-lived |
| `ORDERED` | Sent for manufacture and shipping |
| `RECEIVED` | **The cardholder has set their PIN.** Not a delivery confirmation |


Two of these mislead if read at face value.

**`INITIAL` can last a while in production.** Card orders are batch processed periodically, and an order sits in `INITIAL` until the batch picks it up. In sandbox nothing is manufactured at all, so `POST /api/v1/sandbox/generate-card/` is what advances an order — without it a sandbox order never reaches a card you can transact on.

**`RECEIVED` means the PIN was set, not that the card arrived.** Jiko has no delivery signal; setting the PIN is the closest proxy, and it is what moves the order to `RECEIVED`. An order that stays in `ORDERED` tells you the cardholder has not set a PIN — which may mean the card is lost in the post, or simply that they have not got round to it.

`card_id` is `null` until the order reaches `CARD_CREATED`, so an integration that reads it immediately after ordering gets nothing. List orders with `GET /api/v1/jiko-accounts/{account_id}/card-orders/` or `GET /api/v1/customers/{customer_id}/card-orders/`, both cursor-paginated, and fetch one with `GET /api/v1/jiko-accounts/{account_id}/card-orders/{card_order_id}/`.

## Card status

| Status | Settable by you | Meaning |
|  --- | --- | --- |
| `NOT_ACTIVATED` | No | Issued, no PIN set. Setting the PIN moves it to `OPEN` |
| `OPEN` | Yes | Active |
| `LOCKED` | Yes | Temporarily blocked by you or the cardholder |
| `FROZEN` | No | Suspended by Jiko |
| `CLOSED` | No — use the close endpoint | Terminal |


```
POST /api/v1/jiko-accounts/{account_id}/cards/{card_id}/status/
POST /api/v1/jiko-accounts/{account_id}/virtual-cards/{card_id}/status/
```

accepts `status` (`OPEN` or `LOCKED` only) and `magstripe_status` (`ON` or `OFF`). Magstripe is off by default on physical cards; turning it on widens the fraud surface, so only do it where a cardholder actually needs it.

`GET` on the same path returns `card_id`, `status` and `magstripe_status`.

### Closing a card

```
POST /api/v1/jiko-accounts/{account_id}/cards/{card_id}/close/
```

| Field | Notes |
|  --- | --- |
| `closure_reason` | `LOST`, `STOLEN`, `DAMAGED`, `NEVER_RECEIVED`, `EXPIRED` or `CANCELED` |
| `lost_stolen_date` | **Required** when the reason is `LOST` or `STOLEN`; **rejected** for any other reason. Cannot be in the future |


Closure is terminal. Replacing a card means ordering a new one.

Virtual cards are closed with `DELETE /api/v1/jiko-accounts/{account_id}/virtual-cards/{card_id}/`, which returns `204`.

## Setting a PIN

```
POST /api/v1/jiko-accounts/{account_id}/cards/{card_id}/pin/
```

The body carries `encrypted_pin`: the cardholder's 4-digit PIN, encrypted with Jiko's `set_pin` public key and base64-encoded. Fetch that key from:

```
GET /api/v1/public-keys/
```

The PIN never travels in the clear, and Jiko never returns it. The endpoint answers `{ "success": true }`.

```bash
curl --request GET --url "https://$PREFIX.sandbox-api.jikoservices.com/api/v1/public-keys/" \
  | jq -r '.set_pin' > set_pin.pub

pin="1234"
encrypted_pin=$(echo $pin | openssl rsautl -encrypt -pubin -inkey set_pin.pub | base64)

body='{"encrypted_pin": "'$encrypted_pin'"}'
send_jiko_request "POST" "/api/v1/jiko-accounts/$account_id/cards/$card_id/pin/" $body
```

### Setting the PIN is what activates a physical card

This is the step integrations leave out, and the card simply does not work without it.

A physical card is issued `NOT_ACTIVATED`. Setting the PIN does three things at once: it stores the PIN, it moves the card from `NOT_ACTIVATED` to `OPEN`, and it moves the card **order** to `RECEIVED`. Until then the cardholder cannot swipe, dip, lock or open the card.

The same endpoint changes a PIN later; on a card that is already `OPEN` it only stores the new PIN.

Preferably the PIN is set once the cardholder has the card in hand, but it can be set as soon as the order reaches `CARD_CREATED`.

## Spending controls

```
GET  /api/v1/jiko-accounts/{account_id}/cards/{card_id}/limits/
POST /api/v1/jiko-accounts/{account_id}/cards/{card_id}/limits/
```

| Field | Notes |
|  --- | --- |
| `allowed_categories` | Merchant category codes the card may be used at |
| `blocked_categories` | Merchant category codes the card may not be used at |
| `spending_limits` | A list of `{ amount, interval }` |


Intervals: `PER_AUTHORIZATION`, `DAILY`, `WEEKLY`, `MONTHLY` (the default), `YEARLY`, `ALL_TIME`.

Limits are per card. Set them at issuance rather than after the first transaction — a card with no limits set carries whatever the program default is.

## Webhooks

**Card status:**

| Event |
|  --- |
| `card.status.open` |
| `card.status.locked` |
| `card.status.frozen` |
| `card.status.closed` |


Payload: `card_id`, `status`.

**Card transactions:**

| Event | Meaning |
|  --- | --- |
| `card.transaction.approved` | Authorization approved |
| `card.transaction.on_hold` | Authorization placed on hold |
| `card.transaction.rejected` | Authorization declined |
| `card.transaction.reversed` | A prior authorization was reversed |


Payload: `card_id`, `amount`, `account_id`.

The transaction payload carries an amount, which is unusual among Jiko's events — most carry only identifiers. It is still worth reading the transaction feed for the settled picture, since an authorization is not a settlement and a reversal arrives as its own event.

## Endpoints

| Operation | Endpoint |
|  --- | --- |
| List a pocket's cards | `GET /api/v1/jiko-accounts/{account_id}/cards/` |
| List a customer's cards | `GET /api/v1/customers/{customer_id}/cards/` |
| Create a virtual card | `POST /api/v1/jiko-accounts/{account_id}/virtual-cards/` |
| Close a virtual card | `DELETE /api/v1/jiko-accounts/{account_id}/virtual-cards/{card_id}/` |
| Get a virtual card's numbers | `POST /api/v1/jiko-accounts/{account_id}/virtual-cards/{card_id}/` |
| Get a physical card's numbers | `POST /api/v1/jiko-accounts/{account_id}/physical-cards/{card_id}/` |
| Order a physical card | `POST /api/v1/jiko-accounts/{account_id}/card-orders/` |
| List card orders (pocket) | `GET /api/v1/jiko-accounts/{account_id}/card-orders/` |
| List card orders (customer) | `GET /api/v1/customers/{customer_id}/card-orders/` |
| Get a card order | `GET /api/v1/jiko-accounts/{account_id}/card-orders/{card_order_id}/` |
| Get / set card status | `GET`/`POST /api/v1/jiko-accounts/{account_id}/cards/{card_id}/status/` |
| Get / set virtual card status | `GET`/`POST /api/v1/jiko-accounts/{account_id}/virtual-cards/{card_id}/status/` |
| Get / set limits | `GET`/`POST /api/v1/jiko-accounts/{account_id}/cards/{card_id}/limits/` |
| Set or change PIN | `POST /api/v1/jiko-accounts/{account_id}/cards/{card_id}/pin/` |
| Close a card | `POST /api/v1/jiko-accounts/{account_id}/cards/{card_id}/close/` |
| Public keys | `GET /api/v1/public-keys/` |


## Reading card spend

Card activity appears in the transaction feed as `type: "CARD"`:

| Operation | Endpoint |
|  --- | --- |
| Pocket transactions | `GET /api/v2/pockets/{pocket_id}/transactions/` |
| Customer transactions | `GET /api/v2/customers/{customer_id}/transactions/` |
| All partner transactions | `GET /api/v2/transactions/` |


Filter with `filter[type]=CARD`.

## Testing in sandbox

Two sandbox-only endpoints make the card flow exercisable end to end without a physical card:

| Operation | Endpoint |
|  --- | --- |
| Simulate a card swipe | `POST /api/v1/sandbox/card-swipe/` |
| Generate a physical card from an order | `POST /api/v1/sandbox/generate-card/` |
| Trigger a webhook | `POST /api/v1/sandbox/webhook/` |
| Fund a portal | `POST /api/v1/sandbox/fund/` |


`generate-card` is what moves a sandbox card order past `ORDERED` — nothing is actually manufactured, so without it the order never reaches a card you can transact on.

## See also

- [Cards reference](/products/partner-api/reference/manage-cards)
- [Webhooks](/products/partner-api/guides/building/webhooks)
- [Sandbox](/products/partner-api/guides/onboarding/partner-onboarding)