From merchant registration to a user-approved test request
This is the canonical onboarding order for the official testnet origin https://api-testnet.velarumpay.com. A local offline mock exercises the same order without network access or credentials.
0. Run the offline contract first
node examples/generic-payment-request/merchant-quickstart.mjs
The zero-dependency mock completes registration, domain verification, Recipient and Agent setup, Agent token exchange, Connect, and a pending Payment Request. It uses reserved .invalid domains and labeled test values.
1. Register and verify the merchant account
Submit the operator email to POST /v1/merchant-accounts/registrations, then follow the email link through POST /v1/merchant-accounts/email-verifications. Registration returns a generic response to prevent account enumeration. Verification returns a one-time session grant.
2. Create the organization and verify its domain
Create the organization with POST /v1/merchants using the one-time grant, then exchange the returned next grant at POST /v1/merchant-sessions. The browser receives an HttpOnly session cookie and an in-memory CSRF proof. Register an exact domain using POST /v1/merchant-domains, publish only the returned DNS or HTTPS challenge, then call POST /v1/merchant-domains/{id}/verify. Do not fetch or probe a user-entered domain from browser code.
active; do not use an operator credential or an undocumented route to bypass verification.3. Register, verify, and activate a Recipient
const recipient = await merchant.createRecipient({
name: "Test Recipient",
network: "stellar_testnet",
asset: "XLM",
address: recipientTestnetAddress
});
const verification = await merchant.verifyRecipient(recipient.recipient.recipient_id, {
recipient_version_id: recipient.recipient_version.recipient_version_id,
type: "domain_proof",
source_id: verifiedEvidenceSourceId,
version: recipient.recipient.version
});
// Activate only after evidence succeeds and the cooling period completes.
await merchant.activateRecipientVersion(
recipient.recipient.recipient_id,
recipient.recipient_version.recipient_version_id,
{
evidence_id: verification.evidence.evidence_id,
evidence_digest: verification.evidence.evidence_digest,
version: nextRecipientVersion
}
);
Never put an address, memo, contract, issuer, or recipient override in display or metadata. Payment creation supplies recipient_id and amount; the Platform API pins the active Recipient version and derives the immutable payment facts.
4. Create an Agent and register its public credential
const agent = await merchant.createAgent({
name: "Checkout Agent",
runtime: "node",
environment: "testnet",
allowed_scopes: ["payment.requests.create", "payment.requests.read"]
});
const credential = await merchant.registerAgentCredential(agent.agent.agent_id, {
kid: credentialId,
alg: "Ed25519",
public_key: exportedPublicKey,
not_before: credentialNotBefore,
expires_at: credentialExpiresAt
});
await merchant.activateAgentCredential(
agent.agent.agent_id,
credential.credential.credential_id,
{ challenge: credential.proof_challenge, signature: localProofSignature }
);
await merchant.updateAgent(agent.agent.agent_id, { status: "active", version: 1 });
The registration response contains a five-minute proof challenge. Sign that challenge locally before activation. Generate and retain signing material in your secure local signer; upload only the public key and proof of possession. See Agent Auth for the exact assertion fields.
5. Exchange a local Agent Assertion for a short token
import { createVelarumPayClient } from "@velarumpay/sdk-js";
const agentClient = createVelarumPayClient({
baseUrl: "https://api-testnet.velarumpay.com",
agentAuth: {
agentId,
clientId,
kid: credentialId,
scopes: ["payment.requests.create", "payment.requests.read"],
keyHandle: secureKeyHandle,
signer: ({ data, keyHandle }) => secureSigner.sign(data, keyHandle)
}
});
await agentClient.exchangeAgentToken();
The SDK holds the short token in memory and coordinates refresh. It retries one Agent-authenticated request after a 401; it does not loop on 403, revocation, clock skew, or duplicate JTI.
6. Claim the user's one-time Connect Key
const claim = await agentClient.claimConnectInvitation(connectKey, [
"payment.requests.create",
"payment.requests.read"
]);
// The user reviews verified merchant + Agent identity and scopes in VelarumPay.
const connection = await agentClient.exchangeConnectInvitation(
claim.connect_invitation.connect_invitation_id,
claim.claim_token
);
A Connect Key and claim token are one-time coordination credentials, not approval or signing authority. The exchange succeeds only after wallet approval and returns a scoped connection token.
7. Create a test Payment Request
const created = await agentClient.createPaymentRequest({
recipient_id: recipientId,
recipient_version_id: activeRecipientVersionId,
payment: { amount: "1.00" },
expires_at: expiresAt,
policy: { requires_user_approval: true }
}, {
idempotencyKey: "test-payment-1",
requestId: requestId
});
const detail = await agentClient.getPaymentRequest(created.payment_request.request_id);
const events = await agentClient.pollEvents({ since: 0 });
8. Add a signed Webhook
Create and verify the endpoint in Merchant Portal, display the generated endpoint secret once, and store it only in your server-side secret manager. Verify the exact raw body before parsing JSON; see the Webhook verification guide and fixed vector.
Contracts and errors
Download the OpenAPI 3.1 contract and protocol schemas. Errors are stable envelopes with error.code, error.message, and error.request_id. Cross-tenant lookups use a non-enumerating resource_not_found response.