Skip to content

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_typeMeaning
spendCard purchase
collateralShared collateral movement
paymentCard balance payment
feeFee charge
transferTransfer
fundingFunds moved into collateral
withdrawalFunds 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_typeMeaning
api.chargeFee charged through the API
api.backfillTransaction added or reconciled from an API snapshot
transfer.fundingTransfer into collateral
transfer.withdrawalTransfer 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.