Developer Preview · Test and pilot use only · View program boundaries
V VelarumPay Docs
Merchant Quickstart · Testnet

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.

This flow never asks for a wallet private key and never grants payment-signing authority to an Agent. Recipient facts come from an active verified Recipient version; the wallet still reviews and approves every payment.

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.

Domain proof is a gate, not a client-side activation command. Stop here until the Platform API reports the merchant as 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 });
A successful create means “pending wallet review,” not “paid.” Use exact decimal strings, a stable idempotency key, and the returned request ID. Never auto-sign, auto-approve, or treat a Webhook as the source of truth.

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.