Introduction
The Concordex API lets your software send and swap Protocol-Level Tokens on Concordium through a small set of HTTPS endpoints. Pay in any PLT, receive in any PLT — cross-PLT settlement is a one-call primitive, not a two-leg dance. Your project's wallet keys live inside a hardened vault; your code authenticates with a scoped API key and never touches the chain directly.
Base URL
https://api.concordex.app Versioning — current major version is v1, included in the URL path. Breaking changes ship as a new major version; previous version supported for ≥12 months past deprecation.
Authentication
Every request must include a project API key as a bearer token:
Authorization: Bearer sk_live_a1b2c3d4e5f6g7h8 Keys are scoped to a single project and to a capability set (payments:write, payments:read, balances:read, keys:admin). Today the same base URL serves Concordex devnet (our P10 Concordium chain) for both sk_test_… and sk_live_… keys; mainnet routing flips on once the PLT-lock primitive lands on chain.
Quickstart
The minimum viable integration is one HTTP call:
curl https://api.concordex.app/v1/payments \
-H "Authorization: Bearer sk_live_a1b2c3d4e5f6g7h8" \
-H "Idempotency-Key: pay_2026_05_11_invoice_4831" \
-H "Content-Type: application/json" \
-d '{
"amount": "5.00",
"token": "CCDX",
"to": "agent_4f2a91…",
"memo": "Invoice 4831",
"metadata": { "customer_id": "cust_8821" }
}' Response (HTTP 201):
{
"id": "pay_01HV5K3Q8R7TQ9F2WJZX1Y4M9C",
"object": "payment",
"status": "submitted",
"amount": "5.00",
"token": "CCDX",
"to": "agent_4f2a91…",
"tx_hash": "f8a4c6d2…",
"created_at": "2026-05-11T14:22:31Z",
"finalised_at": null
} Errors
Conventional HTTP status codes. Error bodies are JSON:
{
"error": {
"type": "policy_violation",
"code": "daily_cap_exceeded",
"message": "This payment would exceed the project daily cap of 10,000 CCD.",
"param": "amount",
"request_id": "req_01HV5K3QX9F7T2WJZ"
}
} | Status | Type | Meaning |
|---|---|---|
400 | invalid_request | Malformed JSON, missing field, bad parameter shape. |
401 | authentication_error | Missing, invalid, or revoked API key. |
403 | policy_violation | Valid request blocked by your project policy (cap, allow-list, etc.). |
409 | idempotency_conflict | Same idempotency key used for a different request body. |
429 | rate_limited | Too many requests on this key. Retry with backoff. |
503 | chain_unavailable | Concordium node temporarily unreachable. Safe to retry. |
Create a payment
/v1/paymentsParameters
| Field | Type | Req? | Description |
|---|---|---|---|
amount | string | yes | Decimal string, e.g. "5.00". |
token | string | yes | PLT symbol (e.g. "CCDX") or PLT identifier. Use "CCD" for the chain's native token (CCD is not itself a PLT — it's the universal counter-asset). |
to | string | yes | Concordium account address of the recipient. |
memo | string | no | Optional on-chain memo, ≤256 bytes UTF-8. |
metadata | object | no | Off-chain key/value pairs, returned on retrieve and in webhooks. |
require_approval | bool | no | If true, payment enters pending_approval until a second key confirms. |
Python example
import os, requests
resp = requests.post(
"https://api.concordex.app/v1/payments",
headers={
"Authorization": f"Bearer {os.environ['CONCORDEX_KEY']}",
"Idempotency-Key": "pay_2026_05_11_invoice_4831",
},
json={
"amount": "5.00",
"token": "CCDX",
"to": "agent_4f2a91…",
"metadata": {"customer_id": "cust_8821"},
},
timeout=10,
)
resp.raise_for_status()
payment = resp.json()
print(payment["id"], payment["status"], payment["tx_hash"]) Cross-token payments (pay_with)
Set pay_with to the token you want to debit when it differs from the token the receiver should land in. Concordex quotes the matching pool, executes the swap, and settles atomically. Today the orchestrator mediates settlement through a Concordex treasury account on devnet; once PLT-locks land on chain, the same call routes through LP locks without Concordex touching principal. Slippage and allowed-pair policy apply.
POST /v1/payments
{
"pay_with": "CCDX", // debit your wallet in this token
"to": "agent_4f2a91…",
"amount": "42.00", // amount the receiver lands with
"token": "EURX", // receiver's token (any live PLT works)
"max_slippage": "0.30%", // optional, defaults to project policy
"idempotency_key": "pay_2026_05_11_0001"
} Response includes swap_quote_id and the effective rate alongside the usual payment fields. The payment is rejected (no debit, no settle) if the quote would breach the slippage cap.
Retrieve a payment
/v1/payments/{id}Returns the current payment row. status evolves through:
pending_approval— created withrequire_approval=true.submitted— signed by the vault, sent to Concordium.finalised— finality reached on-chain.finalised_at+feepopulated.rejected— chain rejected the transaction. Terminal.failed— policy violation discovered after submission. Terminal.
List payments
/v1/paymentsCursor-paginated, newest first. Query: limit (default 20, max 100), starting_after, status, token, created_gte, created_lte.
Balances
/v1/balances{
"object": "list",
"data": [
{ "token": "CCD", "balance": "12450.75", "decimals": 6 },
{ "token": "CCDX", "balance": "8200.00", "decimals": 6 }
]
} Quote a swap
/v1/swaps/quoteReturns an indicative rate + a short-lived quote_id you can pass to POST /v1/swaps. No funds move. Quotes expire after 30 seconds. Direct pairs are PLT↔CCD; cross-PLT trades return a multi-hop route.
curl https://api.concordex.app/v1/swaps/quote \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"from": "EURX",
"to": "CCDX",
"amount_in": "1000"
}' {
"quote_id": "quo_01HV5P9F2C…",
"from": "EURX",
"to": "CCDX",
"amount_in": "1000.00",
"amount_out": "2714.55",
"rate": "2.71455",
"route": ["EURX/CCD", "CCD/CCDX"],
"expires_at": "2026-05-11T14:23:01Z"
} Execute a swap
/v1/swapsAtomic swap from the project wallet. Pairs are PLT↔CCD; cross-PLT trades route through CCD in a single atomic call. Either pass a fresh quote_id for guaranteed rate (within slippage), or pass max_slippage and let Concordex quote-then-execute under the cap.
curl https://api.concordex.app/v1/swaps \
-H "Authorization: Bearer sk_live_…" \
-H "Idempotency-Key: swap_2026_05_11_treasury_0001" \
-H "Content-Type: application/json" \
-d '{
"from": "EURX",
"to": "CCDX",
"amount_in": "1000",
"max_slippage": "0.30%"
}' {
"id": "swp_01HV5PA7K3…",
"status": "finalised",
"from": "EURX",
"to": "CCDX",
"amount_in": "1000.00",
"amount_out": "2713.34",
"rate": "2.71334",
"fee": { "token": "CCD", "amount": "0.0042" },
"tx_hash": "f8a4c6d2…",
"finalised_at": "2026-05-11T14:22:46Z"
} Swap parameters
| Field | Type | Req? | Description |
|---|---|---|---|
from | string | yes | Source token — a PLT symbol/identifier, or "CCD" for the native counter-asset. |
to | string | yes | Destination token — a PLT symbol/identifier, or "CCD". Direct pairs are PLT↔CCD; cross-PLT trades route through CCD automatically. |
amount_in | string | one of | Decimal amount to debit. Mutually exclusive with amount_out. |
amount_out | string | one of | Decimal amount the project should receive. Concordex computes the required amount_in. |
max_slippage | string | no | Cap as a percentage, e.g. "0.30%". Defaults to project policy. |
quote_id | string | no | Pre-fetched quote from POST /v1/swaps/quote. If absent, Concordex quotes inline. |
recipient | string | no | Optional — land the destination PLT on a different Concordium account (useful for pay-and-swap flows). |
Today swaps run against per-pair pools mediated by a Concordex treasury account on devnet (constant-product AMM, 30 bps LP fee). Reserves and LP positions live in the Concordex ledger; each swap settles as two on-chain PLT transfers. When Concordium ships the PLT-lock primitive in protocol, the same API call settles directly against LP locks: LPs delegate scoped, capped amounts of their PLTs with per-swap caps, per-period caps and expiry, and Concordex only holds release-signing authority — principal stays with the LP. The API surface, schema and pricing engine are unchanged through that migration; only the three settlement helpers get rewired.
CCDX staking
Every swap pays a 10 bps protocol fee on top of the LP fee. The fee accumulator is swept periodically, converted to CCD via the per-token pools, and split 45% to stakers, 45% to a 365-day lock wallet, 10% to the protocol treasury. Stakers earn CCDX pro-rata, distributed via a Synthetix-style accumulator (O(1) per sweep). Live state + APR: /stake.
/v1/stakeThe caller's CCDX position — staked principal + pending rewards (accrued via reward-per-share since last touch).
/v1/stakeStake N CCDX. The vault signs the transfer caller → staking pool; the API updates the accumulator atomically.
POST /v1/stake
{
"amount": "100"
} /v1/stake/withdrawWithdraw N CCDX from your principal. Pending rewards stay accrued — claim separately.
/v1/stake/claimPay out accrued CCDX rewards to your wallet. Zero on first claim or right after a previous claim.
/v1/stake/statsGlobal rewards state — total staked, accumulator, 7-day rolling APR estimate.
/v1/protocol/feesUnswept per-token fee accumulator + last completed sweep epoch + lock-wallet totals.
API keys
/v1/projects/keysThe full secret is returned exactly once. Store it on your side immediately.
curl https://api.concordex.app/v1/projects/keys \
-H "Authorization: Bearer sk_live_admin_…" \
-H "Content-Type: application/json" \
-d '{
"name": "billing-worker",
"capabilities": ["payments:write", "payments:read"],
"rate_limit_per_minute": 60,
"daily_cap": { "token": "CCD", "amount": "5000" }
}' Webhook events
Register a HTTPS endpoint per project. We send a signed POST on every state transition. Acknowledge with HTTP 2xx within 10 seconds; otherwise we retry with exponential backoff up to 24 hours.
| Event | When it fires |
|---|---|
payment.submitted | Signed by the vault and sent to Concordium. |
payment.finalised | Finality reached on-chain. |
payment.rejected | Chain rejected. Terminal. |
payment.failed | Internal policy failure post-submission. Terminal. |
key.revoked | An API key under this project was revoked. |
policy.updated | Project policy changed. |
Webhook signature
Header: Concordex-Signature: t=<timestamp>,v1=<hmac-sha256>. Verify on your side:
import hmac, hashlib, time
def verify(payload: bytes, sig_header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in sig_header.split(","))
ts, v1 = parts["t"], parts["v1"]
if abs(time.time() - int(ts)) > tolerance:
return False
signed = f"{ts}.".encode() + payload
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1) Python SDK
pip install concordex from concordex import Concordex
client = Concordex(api_key="sk_live_a1b2c3d4e5f6g7h8")
payment = client.payments.create(
amount="5.00",
token="CCDX",
to="agent_4f2a91…",
idempotency_key="pay_2026_05_11_invoice_4831",
)
final = client.payments.wait_for_finality(payment.id, timeout=30)
print(final.tx_hash, final.fee) TypeScript SDK
npm install @concordex/sdk import { Concordex } from "@concordex/sdk";
const client = new Concordex({ apiKey: process.env.CONCORDEX_KEY! });
const payment = await client.payments.create({
amount: "5.00",
token: "CCDX",
to: "agent_4f2a91…",
idempotencyKey: "pay_2026_05_11_invoice_4831",
});
const final = await client.payments.waitForFinality(payment.id, { timeoutMs: 30_000 });
console.log(final.tx_hash, final.fee); MCP tool surface
For agents using the Model Context Protocol, Concordex exposes the same endpoints as MCP tools. Drop into Claude Desktop, Cursor, Cline:
pip install concordex-mcp
concordex-mcp --api-key sk_live_a1b2c3d4e5f6g7h8 {
"mcpServers": {
"concordex": {
"command": "concordex-mcp",
"env": { "CONCORDEX_KEY": "sk_live_a1b2c3d4e5f6g7h8" }
}
}
} Hosted MCP App
Don't want to run the MCP server yourself? Use the Concordex Hosted MCP App — a managed MCP endpoint plus a web UI for the human side of agent payments: see every payment your agent triggered, who approved what, balances per PLT, policy edits, audit tail.
Claude Desktop config
{
"mcpServers": {
"concordex": {
"url": "https://mcp.concordex.app/v1/your-project",
"transport": "http",
"headers": { "Authorization": "Bearer mcp_live_…" }
}
}
} The MCP App can require dashboard approval on payments above a threshold — the agent gets a pending_approval response, a human ticks the box in the UI.
Two surfaces, one wallet. Hosted MCP App and the REST API both authorise the same project wallet against the same vault. Use whichever surface fits the caller.