Skip to content

Partner Card API

The Partner Card API gives external card-issuing partners programmatic access to card management via a unified GraphQL endpoint. Partners can query card data (via Hasura) and execute card operations (via the card issuer) from the same URL.

Endpoint: https://api.agiodigital.com/partner/cards/graphql

Interactive explorer: open the endpoint URL in a browser to access the built-in GraphiQL IDE with HMAC signing.

Access Required

Partner API credentials are provisioned by Agio ops. Contact your Agio account manager to request access. You will receive an api_token UUID and a client_secret for request signing.

GraphiQL Explorer

Open https://api.agiodigital.com/partner/cards/graphql in your browser to access the interactive GraphQL IDE. The explorer includes:

  • Schema browser — browse all available queries and mutations without credentials (introspection is unauthenticated)
  • Credential panel — paste your api_token and client_secret to execute signed requests directly from the browser
  • Auto-signing — the explorer signs every POST request with HMAC-SHA256 using your client secret via WebCrypto

Credentials are stored in sessionStorage only and cleared when the tab closes.

Authentication

Every execution request requires two layers of authentication. Schema introspection and the GraphiQL explorer work with just the API key (no signature needed).

Layer 1 — API Key

Send your api_token as the x-agio-api-key header:

x-agio-api-key: <your-api-token-uuid>

Layer 2 — HMAC Signature

Sign each execution request with your client_secret using HMAC-SHA256 over `${timestamp}.${rawBody}`:

HeaderValue
x-agio-timestampCurrent Unix time in milliseconds (Date.now())
x-agio-signatureHMAC-SHA256 hex digest of `${timestamp}.${rawBody}`

Timestamp in Milliseconds

The timestamp must be in milliseconds (e.g. 1744736599000), not seconds. Requests outside ±5 minutes of server time are rejected.

Introspection exception: queries containing only __schema or __type fields bypass the HMAC check — you can discover the schema with just your API key.

Replay protection: each (token, timestamp, signature) tuple is accepted only once within a 10-minute window.

Signing Requests

Node.js / TypeScript

typescript
import { createHmac } from "node:crypto";

const endpoint = "https://api.agiodigital.com/partner/cards/graphql";
const apiToken = process.env.AGIO_PARTNER_API_TOKEN!;
const clientSecret = process.env.AGIO_PARTNER_CLIENT_SECRET!;

async function partnerQuery(query: string, variables?: Record<string, unknown>) {
  const body = JSON.stringify({ query, variables });
  const ts = Date.now().toString();
  const sig = createHmac("sha256", clientSecret).update(`${ts}.${body}`).digest("hex");

  const res = await fetch(endpoint, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "x-agio-api-key": apiToken,
      "x-agio-timestamp": ts,
      "x-agio-signature": sig
    },
    body
  });

  return res.json();
}

Python

python
import hmac, hashlib, time, json, os, urllib.request

def partner_query(query: str, variables: dict | None = None) -> dict:
    endpoint = "https://api.agiodigital.com/partner/cards/graphql"
    api_token = os.environ["AGIO_PARTNER_API_TOKEN"]
    client_secret = os.environ["AGIO_PARTNER_CLIENT_SECRET"]

    body = json.dumps({"query": query, "variables": variables}).encode()
    ts = str(int(time.time() * 1000))
    sig = hmac.new(client_secret.encode(), f"{ts}.".encode() + body, hashlib.sha256).hexdigest()

    req = urllib.request.Request(endpoint, data=body, method="POST")
    req.add_header("Content-Type", "application/json")
    req.add_header("x-agio-api-key", api_token)
    req.add_header("x-agio-timestamp", ts)
    req.add_header("x-agio-signature", sig)

    with urllib.request.urlopen(req, timeout=15) as r:
        return json.loads(r.read())

bash / curl

For quick manual testing or shell scripts:

bash
ENDPOINT="https://api.agiodigital.com/partner/cards/graphql"
BODY='{"query":"{ AgioCard_card_company { id } }"}'
TS=$(python3 -c 'import time; print(int(time.time()*1000))')
SIG=$(printf '%s' "${TS}.${BODY}" | openssl dgst -sha256 -hmac "$AGIO_PARTNER_CLIENT_SECRET" -hex | awk '{print $NF}')

curl -sS -X POST "$ENDPOINT" \
  -H "content-type: application/json" \
  -H "x-agio-api-key: $AGIO_PARTNER_API_TOKEN" \
  -H "x-agio-timestamp: $TS" \
  -H "x-agio-signature: $SIG" \
  -d "$BODY"

Expected: {"data":{"AgioCard_card_company":[...]}}. A 401 means your signature is off — check the Best practices section for common causes.

Verify your setup

Before writing integration code, confirm your API token is active and your environment is reachable. Introspection works with just the API key — no HMAC signing required — so this is the fastest smoke test:

bash
curl -sS -X POST https://api.agiodigital.com/partner/cards/graphql \
  -H "content-type: application/json" \
  -H "x-agio-api-key: $AGIO_PARTNER_API_TOKEN" \
  -d '{"query":"{ __schema { queryType { name } mutationType { name } } }"}'

Expected response:

json
{
  "data": {
    "__schema": {
      "queryType": { "name": "Query" },
      "mutationType": { "name": "Mutation" }
    }
  }
}

If you see {"error":"Unauthorized","description":"Invalid API key"} instead, the token is either mistyped, revoked, or scoped to the wrong environment. Contact your Agio account manager to verify.

To test end-to-end signing, run the Node.js or Python example above with a cardBalance query (expect a success: false, error: "Card not found" if you don't have a card ID yet — that confirms the sign-and-execute path works).

Schema Overview

The partner endpoint is a unified graph stitched from two sources:

SourcePrefixOperationsDescription
HasuraAgioCard_*34 queriesRead card data, users, balances, transactions
Platform APICard* / utility names1 query + 22 mutationsOnboarding, card lifecycle, operations, funding

All results are automatically scoped to your partner organization.

Available Mutations

Onboarding:createPartnerCustomerOrganization · createCardApplicationForPartnerUser

Card lifecycle:createCard · replaceCard · replaceVirtualCard · cancelCard

Card operations:freezeCard · unfreezeCard · lockCard · unlockCard · revealCardSecrets · setCardPin · getCardPin · updateCardNickname · updateCardLimit

Profile & address:updateCardUserProfile · updateCardCompanyAddress · validateAddress · validateCardShippingAddress · autocompleteAddress · resolvePlaceAddress

Funding:chargeCard

Not exposed via this endpoint

  • createCardApplication — use createCardApplicationForPartnerUser instead (partner-specific resolver that skips Agio-user KYC)
  • createCardCorporateApplication — deferred; the underlying resolver requires an authenticated Agio user in context. A partner-aware corporate onboarding flow will ship in a later phase. Contact your Agio account manager if you need corporate applications provisioned in the interim (Agio ops can create them on your behalf).
  • cardWithdraw — planned for a future release (depends on cardWithdraw refactor)
  • payInvoiceWithCardBalance — Agio-internal billing flow, not relevant to partner integrations
  • All *ByCardId / *ByCardUserId admin-bypass operations

These mutations may appear in schema introspection (they're part of the underlying SDL) but execution is blocked at the Shield layer with extensions.code === "FORBIDDEN".

Onboarding Flow

Provision a customer organization

graphql
mutation {
  createPartnerCustomerOrganization(input: { name: "Acme Corp" }) {
    success
    organizationId
  }
}

Each customer organization is scoped to your partner account and isolated from other partners. Use organizationId when coordinating with Agio ops for corporate application provisioning (currently a manual step — see Not exposed via this endpoint).

Individual card application (partner-provisioned user)

graphql
mutation {
  createCardApplicationForPartnerUser(
    input: {
      cardUserId: "rain-user-uuid"
      walletAddress: "0x1234...abcd"
      occupation: "Software Developer"
      annualSalary: "100000"
      accountPurpose: "Business expenses"
      expectedMonthlyVolume: "5000"
      isTermsOfServiceAccepted: true
    }
  ) {
    success
    cardApplicationId
    applicationStatus
  }
}

PIN encryption (setCardPin / getCardPin)

PIN values transit a per-session encrypted channel. The handshake derives a transfer key from your partner organization id — the UUID returned by createPartnerCustomerOrganization for your home tenant, or the partner_organization_id field exposed by your token row. Use it as the passphrase identity when calling encryptPassphraseForTransfer from agio-utils:

typescript
import { encryptPassphraseForTransfer } from "agio-utils";

const encryptedPin = await encryptPassphraseForTransfer({
  passphrase: "1234",
  sessionId, // returned by your earlier session-init call
  identity: PARTNER_ORGANIZATION_ID // NOT a card user id, NOT the api token
});

await partnerQuery(
  `mutation SetPin($input: SetCardPinInput!) {
    setCardPin(input: $input) { success error }
  }`,
  { input: { cardId, sessionId, encryptedPin } }
);

Identity matches the server-side decryption key

The Platform API decrypts using the same PARTNER_ORGANIZATION_ID it pulls from your token row. Encrypting under any other identity (card user UUID, API token UUID) will decrypt to garbage and the PIN validator will reject it with a generic 400.

Response envelope

Card* mutations share a predictable envelope: every response has a success: Boolean! field and an optional error: String with the human-readable failure reason. Resource-specific fields (e.g. organizationId, cardId, chargeId) appear on success.

graphql
type CardOperationResponse {
  success: Boolean!
  id: Int # our internal card ID
  cardId: String # external card ID
  status: String
  error: String
}

Hasura passthrough queries (e.g. AgioCard_card, AgioCard_card_user) return arrays of typed rows directly — no envelope. Field shapes are enforced by the GraphQL schema itself; use the Partner API Reference for the authoritative type definitions of every operation.

Example Queries

Check a card's balance (Card query)

graphql
query {
  cardBalance(cardId: 42) {
    success
    balance {
      creditLimit
      spendingPower
      balanceDue
    }
  }
}

List cards via Hasura

graphql
{
  AgioCard_vw_card {
    id
    type
    status
    last4
    expiration_month
    expiration_year
    limit_frequency
  }
}

Freeze a card

graphql
mutation {
  freezeCard(cardId: 42) {
    success
    status
    error
  }
}

Cardholders for your organization

graphql
{
  AgioCard_card_user {
    id
    card_user_id
    application_status
    is_active
    wallet_address
  }
}

Monthly spend per user

graphql
{
  AgioCard_vw_card_user_monthly_spend {
    card_user_id
    month
    amount_cents
  }
}

Error Reference

HTTPextensions.codeCause
401Missing/invalid API key, bad signature, expired/revoked token, or timestamp out of window
503Nonce store (Redis) or Hasura temporarily unavailable
400GRAPHQL_PARSE_FAILEDSyntax error in query
400GRAPHQL_VALIDATION_FAILEDQuery references a type outside the partner scope
403FORBIDDENCard/resource belongs to a different partner organization
502UPSTREAM_HASURA_ERRORHasura returned an unexpected error

Example error responses

401 Unauthorized — missing API key:

json
{ "error": "Unauthorized", "description": "Invalid API key" }

401 Unauthorized — HMAC signature mismatch:

json
{ "error": "Unauthorized", "description": "Invalid signature" }

401 Unauthorized — timestamp outside ±5 min window (usually clock skew):

json
{ "error": "Unauthorized", "description": "Timestamp outside allowed window" }

403 FORBIDDEN — cross-partner access attempt (you tried to act on a card or card_user that belongs to a different partner's organization):

json
{
  "data": null,
  "errors": [
    {
      "message": "Not Authorized",
      "extensions": { "code": "FORBIDDEN" }
    }
  ]
}

The error message is intentionally generic — it does NOT reveal whether the resource exists. Do not rely on the message to distinguish "not found" from "exists but forbidden"; both paths return the same shape.

400 GRAPHQL_VALIDATION_FAILED — query references a type outside the partner scope (e.g. attempting to query AgioAuth_user which is not in the stitched schema):

json
{
  "errors": [
    {
      "message": "Cannot query field \"AgioAuth_user\" on type \"Query\".",
      "extensions": { "code": "GRAPHQL_VALIDATION_FAILED" }
    }
  ]
}

Best practices

Secret storage. Store client_secret in your secret manager (AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager). Never commit it to source control, never log it, never send it over unencrypted channels, never include it in URL query strings. The api_token UUID is less sensitive (it only identifies your partner) but should be treated as non-public.

Retry on 503. A 503 response means the nonce store (Redis) or upstream Hasura is transiently unavailable. Retry with exponential backoff starting at 1s, capped at 32s, up to 5 attempts. After 5 failures, alert your on-call — don't silently drop the request.

Retry on 401 is almost always wrong. A 401 means the signature, timestamp, or API key is invalid. Retrying with the same payload will replay the same signature — which the nonce store will reject. If you're seeing intermittent 401s, check (a) clock skew vs. server time, (b) timestamp granularity (must be milliseconds, not seconds), (c) request body serialization stability (the JSON string you sign must match the JSON string you send byte-for-byte).

Idempotency. Mutations are NOT idempotent. If you don't receive a response (network timeout, connection drop), check the resource state via a query before retrying — e.g. query AgioCard_card_application after a createCardApplicationForPartnerUser that timed out.

Credential rotation. Contact your Agio account manager to rotate credentials. Old credentials remain valid until explicitly revoked — there is no automatic expiry. We recommend rotating at least every 12 months, or immediately on any suspected compromise, staff departure, or accidental log exposure. Plan the rotation window: the cutover is instant (the new token is active immediately; the old token can be revoked in the same operation).

Partner Card API has loaded