Appearance
Issue and manage cards
Card operations take the numeric Agio card ID. Store id from createCard and use it as the cardId: Int! argument in later operations. The creation response's string cardId is the issuer's identifier; it appears as card_external_id in read queries.
Create a card
The application must be APPROVED or ACTIVE.
graphql
mutation IssueCard($input: CreateCardInput!) {
createCard(input: $input) {
success
id
cardId
cardType
status
last4
expirationMonth
expirationYear
error
}
}json
{
"input": {
"cardApplicationId": 42,
"cardType": "virtual",
"displayName": "ALICE TESTER",
"limit": { "amount": 50000, "frequency": "per30DayPeriod" }
}
}limit.amount is integer cents: 50000 sets a $500 limit. displayName can be set only at creation and accepts up to 26 characters using ASCII letters, numbers, spaces, periods, and hyphens.
For cardType: "physical", include shipping with line1, city, region, postalCode, countryCode, and phoneNumber. Optional fields include line2, firstName, lastName, and method (standard or express). Shipping names accept up to 50 characters using ASCII letters, spaces, and hyphens; transliterate accented or non-Latin names before submitting. Billing defaults to shipping if omitted. Use validateCardShippingAddress to validate an address before issuance; the reference lists its input.
The issuer cardholder record may arrive after application approval. CARD_USER_NOT_FOUND at this step means its data has not propagated yet; wait and check the application again. MAX_CARDS_REACHED means the cardholder's count of cards that are not canceled or terminated reached the limit configured for your account. Agio sets the limit per account; ask your Agio contact for yours. CARD_LIMIT_EXCEEDED is the issuer-side limit.
After success: true, store both id and the issuer cardId. If the request times out, query the application's cards before repeating issuance.
Read cards and balances
This query returns a customer's cards with their shared collateral and spending balance. Use the issuer card_user_id from the application as the filter value.
graphql
query Cards($where: AgioCard_vw_card_bool_exp!, $offset: Int!) {
AgioCard_vw_card(where: $where, order_by: { id: asc }, limit: 20, offset: $offset) {
id
card_external_id
card_user_id
type
status
last4
expiration_month
expiration_year
limit_amount
limit_frequency
card_application {
id
deposit_address
deposit_chain_id
application_status
}
balance {
credit_limit
collateral_balance
spending_power
balance_due
}
}
}json
{ "where": { "card_user_id": { "_eq": "<cardUserId>" } }, "offset": 0 }Use an empty where object to list all cards visible to your integration. Increase offset by 20 until a page is shorter than 20. Partner reads have a maximum of 100 rows per request; paginate instead of assuming a larger requested limit returns all records.
For a live issuer balance, call cardBalance(cardId: Int!). Its creditLimit, pendingCharges, postedCharges, balanceDue, and spendingPower values are USD amounts, not cents. See balance checks.
AgioCard_card_user lists the cardholders visible to your integration, and AgioCard_vw_card_user_monthly_spend provides monthly total_amount, avg_amount, min_amount, max_amount and transaction_count per currency. Provisioned identity fields such as national identifiers cannot be read back through the partner cardholder query.
Freeze and unfreeze
graphql
mutation Freeze($cardId: Int!) {
freezeCard(cardId: $cardId) {
success
id
status
error
}
}json
{ "cardId": 86 }Use unfreezeCard with the same argument and response selection to restore a frozen card. Check success and the returned status before updating your own record.
Change a spending limit
graphql
mutation ChangeLimit($input: UpdateCardLimitInput!) {
updateCardLimit(input: $input) {
success
id
limitAmount
limitFrequency
error
}
}json
{ "input": { "cardId": 86, "limitAmount": 50000, "limitFrequency": "per30DayPeriod" } }limitAmount is cents. CardLimitFrequency accepts per24HourPeriod, per7DayPeriod, per30DayPeriod, perYearPeriod, allTime, or perAuthorization. There are no daily or monthly aliases. Use updateCardNickname to change a card's nickname; its input is in the reference.
Replace or cancel a card
graphql
mutation Replace($input: ReplaceCardInput!) {
replaceCard(input: $input) {
success
id
oldCardId
newCard {
id
last4
expirationMonth
expirationYear
}
error
}
}json
{ "input": { "cardId": 86, "reason": "lost" } }reason accepts lost, stolen, or damaged. A physical replacement requires shippingAddress. replaceVirtualCard(cardId: Int!) is the virtual-card shorthand and returns the same replacement response.
Replacement cancels the old card. Store the response's top-level id as the replacement's numeric Agio ID, newCard.id as its issuer UUID, and oldCardId as the canceled issuer UUID.
To cancel without a replacement, call cancelCard(input: { cardId: 86, reason: "Customer request" }) and select success, id, status, and error. Cancellation is irreversible.
Encrypt PINs and card secrets
PIN changes and card-secret reads use a short-lived encryption session in addition to normal request signing. Generate a fresh session immediately before each operation:
graphql
mutation EncryptionSession {
generateEncryptionKeys {
sessionId
key
iv
}
}The session is bound to your partner organization, expires after 15 minutes, and is consumed after successful encryption or decryption on the server. Keep key and iv in memory for the request. Do not reuse a session for another PIN or reveal operation.
The payload format is AES-256-GCM with a 32-byte hex key, a 16-byte hex IV, and a 16-byte authentication tag. The wire string is base64(ciphertext).base64(authTag). These Node.js functions implement that format:
typescript
import { createCipheriv, createDecipheriv } from "node:crypto";
type EncryptionSession = { sessionId: string; key: string; iv: string };
function encryptPin(session: EncryptionSession, pin: string) {
const cipher = createCipheriv("aes-256-gcm", Buffer.from(session.key, "hex"), Buffer.from(session.iv, "hex"));
const ciphertext = Buffer.concat([cipher.update(pin, "utf8"), cipher.final()]);
return `${ciphertext.toString("base64")}.${cipher.getAuthTag().toString("base64")}`;
}
function decryptPayload(session: EncryptionSession, payload: string) {
const [ciphertext, authTag, ...extra] = payload.split(".");
if (!ciphertext || !authTag || extra.length) throw new Error("Invalid encrypted payload");
const decipher = createDecipheriv("aes-256-gcm", Buffer.from(session.key, "hex"), Buffer.from(session.iv, "hex"));
decipher.setAuthTag(Buffer.from(authTag, "base64"));
return Buffer.concat([decipher.update(Buffer.from(ciphertext, "base64")), decipher.final()]).toString("utf8");
}Call encryptPin(session, pin) and send the resulting string as encryptedPin:
graphql
mutation SetPin($input: SetCardPinInput!) {
setCardPin(input: $input) {
success
id
error
}
}json
{ "input": { "cardId": 86, "sessionId": "<fresh sessionId>", "encryptedPin": "<encrypted payload>" } }PINs must contain 4–12 digits and cannot consist of repeated digits or an ascending or descending sequence. You can also supply a fresh sessionId and encryptedPin to createCard: a virtual card's PIN is set at creation; a physical card's PIN is staged until activation.
For a read, create another session and call one of these operations with its sessionId:
graphql
mutation ReadPin($cardId: Int!, $sessionId: String!) {
getCardPin(cardId: $cardId, sessionId: $sessionId) {
success
encryptedPin
error
}
}json
{ "cardId": 86, "sessionId": "<fresh sessionId>" }revealCardSecrets(cardId: Int!, sessionId: String!) returns success, encryptedSecrets, and error. After checking success, decrypt encryptedPin with decryptPayload; it yields the PIN string. Decrypt encryptedSecrets and parse its JSON for pan, cvc, and expiry. Handle the returned expiry shape rather than assuming it is always a formatted string. Keep these values out of logs and persistent card records.
Read transactions
Read transactions through the partner endpoint. This query requires the numeric application ID returned during onboarding and returns up to 20 records with a matching count:
graphql
query Transactions($applicationId: Int!, $offset: Int!) {
transactions: AgioCard_card_transaction(where: { card_application_id: { _eq: $applicationId } }, order_by: [{ created_at: desc }, { id: desc }], limit: 20, offset: $offset) {
id
card_application_id
card_transaction_external_id
card_external_id
card_user_id
event_type
transaction_type
status
amount
currency
local_amount
local_currency
authorized_amount
merchant_name
authorized_at
posted_at
chain_id
transaction_hash
fee_description
created_at
}
transactions_aggregate: AgioCard_card_transaction_aggregate(where: { card_application_id: { _eq: $applicationId } }) {
aggregate {
count
}
}
}json
{ "applicationId": 42, "offset": 0 }Increase offset by 20 until a page is shorter than 20. transactions_aggregate.aggregate.count reports the matching count. Reads are capped at 100 rows per request; fetch every page before calculating totals.
A cardholder query can instead filter card_user_id with your provisioned cardUserId. card_external_id selects one issuer card; it is not the numeric Agio card ID.
Add transaction_type: { _eq: "spend" } to both filters when showing purchases or totaling card spend. You can also filter status, created_at, or posted_at; for example, status: { _eq: "completed" } selects completed records.
Spend amounts use integer minor units: amount: 9500 with currency: "USD" means $95. authorized_amount is the original authorization; local_amount uses local_currency. Fee and funding/withdrawal records created by Agio also store amount in cents, not raw token units.
Authorized purchases are pending and already reduce spending power. Display them alongside settled purchases. Read status for the outcome; it can be null on funding and withdrawal records. Merchant, authorization, and on-chain fields can be null when they do not apply; service fees use fee_description.
transaction_type, event_type, and status are open text values. Handle unfamiliar values as well as these known transaction types:
transaction_type | Meaning |
|---|---|
spend | Card purchase |
collateral | Shared collateral movement |
payment | Card balance payment |
fee | Fee charge |
transfer | Transfer |
funding | Funds moved into collateral |
withdrawal | Funds moved out of collateral |
event_type describes how the record was created. Agio writes these values; processor lifecycle values such as transaction.created can also appear. These are separate from partner webhook event names.
Agio event_type | Meaning |
|---|---|
api.charge | Fee charged through the API |
api.backfill | Transaction added or reconciled from an API snapshot |
transfer.funding | Transfer into collateral |
transfer.withdrawal | Transfer out of collateral |
Transaction webhooks identify the same record through cardTransactionExternalId, which corresponds to card_transaction_external_id in this query. Reconcile current transaction state when events arrive out of order.
Next: Fund cardholder collateral.