Quickstart
From zero to a verifiable credential on Stellar testnet, end to end, with one continuous example. Everything below also works over the raw REST API (see ); this guide uses the React SDK because it is the shortest path.
What you will build
A small React flow that:
- Gets an issuer identity (did:stellar) automatically
- Creates a single-tenant vault for your wallet
- Issues a credential into it
- Verifies it on-chain
Prerequisites
- A Stellar wallet (e.g. Freighter) with a testnet account funded with XLM: use Stellar Lab (fund account). On testnet, issuing charges an on-chain fee of 5 XLM per credential, paid by the issuer.
- A React / Next.js app (the SDK hooks need React 18+;
ActaConfigis a client component). - Node 18+ for tooling.
Step 1: Get an API key
Create your key in the ACTA dApp → API Keys → Create API Key, on the Testnet network, and save it immediately (it is shown only once).
Key facts: one key per wallet per network, standard role, 6-month expiry. The public creation endpoint is origin-restricted, so the dApp is the way to get one. Details in .
Step 2: Install and configure the SDK
npm install @acta-team/credentialsWrap your app with the provider, pointing at testnet:
"use client";
import { ActaConfig, testNet } from "@acta-team/credentials";
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ActaConfig baseURL={testNet} apiKey={process.env.NEXT_PUBLIC_ACTA_API_KEY}>
{children}
</ActaConfig>
);
}You also need a signTransaction callback that asks your wallet to sign. With Freighter:
import { signTransaction } from "@stellar/freighter-api";
const sign = async (xdr: string, opts: { networkPassphrase: string }) => {
const res = await signTransaction(xdr, {
networkPassphrase: opts.networkPassphrase,
});
return res.signedTxXdr;
};Step 3: Get your issuer DID
Issuing requires a registered, resolvable did:stellar. The SDK can create and register it for you with one wallet signature (auto-onboarding):
import { useActaClient } from "@acta-team/credentials";
const client = useActaClient();
const identity = await client.getOrCreateIssuerIdentity({
controller: walletAddress, // your G... account
signTransaction: sign,
});
// identity.did => "did:stellar:testnet:..."The first call generates an Ed25519 key, registers the DID on-chain, and persists the identity (IndexedDB in the browser). Subsequent calls just read it back, no prompt. Full details in the .
Step 4: Create the vault
Each owner has one deterministic, single-tenant vault. Creating it is a one-time operation per wallet:
import { useVault } from "@acta-team/credentials";
const { createVault } = useVault();
const { txId } = await createVault({
owner: walletAddress,
ownerDid: identity.did,
signTransaction: sign,
});If the vault already exists at that address, the on-chain deploy fails with "already deployed": that is safe to treat as success.
Step 5: Issue a credential
import { useCredential } from "@acta-team/credentials";
const { issue } = useCredential();
const { txId } = await issue({
owner: walletAddress, // whose vault receives it
vcId: "employee-badge-001", // unique id, max 64 chars
vcData: {
"@context": ["https://www.w3.org/ns/credentials/v2"],
type: ["VerifiableCredential"],
credentialSubject: {
id: identity.did, // the holder's DID
name: "Ada Lovelace",
role: "Engineer",
},
},
issuer: walletAddress,
signTransaction: sign,
// issuerDid can be omitted: the SDK reuses the identity from Step 3
});One wallet signature later, the credential is encrypted, stored in the vault, and marked valid on-chain. The issuer pays the 5 XLM testnet fee in the same transaction.
Step 6: Verify it
Verification is a free read, open to anyone with an API key:
import { useVaultRead } from "@acta-team/credentials";
const { verifyVc, listVcIds, getVc } = useVaultRead();
const status = await verifyVc({
owner: walletAddress,
vcId: "employee-badge-001",
});
// { status: "valid", since: "..." }
const ids = await listVcIds({ owner: walletAddress });
const vc = await getVc({ owner: walletAddress, vcId: "employee-badge-001" });Or from any terminal against the API:
curl -X POST https://api.testnet.acta.build/contracts/vault/verify-vc \
-H "X-ACTA-Key: your_key" \
-H "Content-Type: application/json" \
-d '{ "owner": "G...", "vcId": "employee-badge-001" }'Full component
The whole flow in one handler:
"use client";
import {
useActaClient,
useVault,
useCredential,
useVaultRead,
} from "@acta-team/credentials";
import { signTransaction } from "@stellar/freighter-api";
export function IssueDemo({ wallet }: { wallet: string }) {
const client = useActaClient();
const { createVault } = useVault();
const { issue } = useCredential();
const { verifyVc } = useVaultRead();
const sign = async (xdr: string, o: { networkPassphrase: string }) =>
(await signTransaction(xdr, { networkPassphrase: o.networkPassphrase }))
.signedTxXdr;
const run = async () => {
const identity = await client.getOrCreateIssuerIdentity({
controller: wallet,
signTransaction: sign,
});
try {
await createVault({ owner: wallet, ownerDid: identity.did, signTransaction: sign });
} catch {
// vault already exists: fine
}
await issue({
owner: wallet,
vcId: "employee-badge-001",
vcData: {
"@context": ["https://www.w3.org/ns/credentials/v2"],
type: ["VerifiableCredential"],
credentialSubject: { id: identity.did, name: "Ada Lovelace" },
},
issuer: wallet,
signTransaction: sign,
});
const status = await verifyVc({ owner: wallet, vcId: "employee-badge-001" });
console.log(status); // { status: "valid", ... }
};
return <button onClick={run}>Issue credential</button>;
}Going further
- - what is on-chain, what is encrypted, who signs what
- - every HTTP error code and how to handle it
- - batch issuance, revocation
- Mainnet: switch
baseURLtomainNet, create a mainnet API key, and make sure the issuer wallet holds USDC with a trustline (the fee is 1 USDC per credential)