@aerocert/sdk
v0.8.1
Published
AeroCert privacy-first SDK for server-to-server certificate issuance.
Maintainers
Readme
@aerocert/sdk
Privacy-first AeroCert SDK for server-to-server issuance.
The SDK mirrors the /tokenize happy path for client software:
- Canonicalize the JSON payload locally.
- Encrypt it locally.
- Compute the SHA-256 hash locally.
- Send only ciphertext, IV, and hash material to
POST /api/v1/privacy/anchorwithX-API-Key. - Return the transaction hash plus verifier artifacts for QR/PDF generation and long-term archive in the client system.
npm install @aerocert/sdkimport { AeroCert, buildPrivacyCertificatePayload } from '@aerocert/sdk';
const aerocert = new AeroCert({
apiKey: process.env.AEROCERT_API_KEY!,
apiBaseUrl: 'https://api.aerocert.co',
privacyKey: process.env.AEROCERT_PRIVACY_KEY!,
});
const payload = await buildPrivacyCertificatePayload({
schema: 'training_certificate@1',
documentType: 'Training Certificate',
chainId: 43113,
chainName: 'Avalanche Fuji Testnet',
contractAddress: process.env.AEROCERT_CERTIFICATE_REGISTRY_ADDRESS!,
issuerOrganizationName: 'Aero Training Org',
issuerOrganizationId: 'org-1',
issuerWallet: '0x1234567890abcdef1234567890abcdef12345678',
recipientName: 'Alice Moreau',
courseName: 'Data Science',
issueDate: '2026-05-19',
certificateNumber: 'MBA-2026-0001',
metadata: {
holder_name: 'Alice Moreau',
training_type: 'Data Science',
training_date: '2026-05-19',
certificate_number: 'MBA-2026-0001',
},
});
const result = await aerocert.issuePrivacyCertificate({
payload,
includeQrDataUrl: true,
includePdf: true,
});
console.log(result.transactionHash);
console.log(result.publicQrUrl);
console.log(result.verificationUrl);
console.log(result.payloadEncoded);
console.log(result.verificationArchive);
console.log(result.qrCode?.dataUrl);
console.log(result.pdfBase64);CommonJS services can use the same package:
const { AeroCert, buildPrivacyCertificatePayload } = require('@aerocert/sdk');Existing API services can also send ordinary certificate JSON through certify(...).
The SDK loads non-secret issuer configuration from AeroCert using the API key,
then builds the verifier-ready payload locally:
const result = await aerocert.certify({
schema: 'generic:training_certificate',
clientReference: 'request-123',
dataToAnchor: {
certificate_number: 'MBA-2026-0001',
holder_first_name: 'Alice',
holder_last_name: 'Moreau',
training_name: 'Aviation Safety',
issued_date: '2026-05-19',
},
}, {
includePdf: true,
includeQrDataUrl: true,
});Timeout, abort, and idempotent retry
Every SDK networked method accepts request controls:
timeoutMs: aborts one AeroCert HTTP request after the given number of milliseconds and rejects withAeroCertTimeoutError.signal: composes a caller-ownedAbortSignalwith the SDK timeout and rejects aborted requests withAbortError.
Timeouts do not prove whether the API completed the operation. For retryable
issuance flows, replay the same logical request with the same idempotencyKey.
import { AeroCert, AeroCertTimeoutError } from '@aerocert/sdk';
const idempotencyKey = `cert-${certificate.id}`;
try {
await aerocert.certify({
schema: 'generic:training_certificate',
clientReference: certificate.id,
dataToAnchor: certificate,
}, {
idempotencyKey,
timeoutMs: 5_000,
});
} catch (error) {
if (error instanceof AeroCertTimeoutError) {
await aerocert.certify({
schema: 'generic:training_certificate',
clientReference: certificate.id,
dataToAnchor: certificate,
}, {
idempotencyKey,
timeoutMs: 15_000,
});
} else {
throw error;
}
}apiKey authenticates the organization. privacyKey encrypts the payload before it leaves the client system; do not treat the API key as the decryption secret or derive the privacy key from it. buildPrivacyCertificatePayload(...) produces the v=1 plaintext shape accepted by AeroCert /verify; arbitrary JSON can still be anchored with the lower-level anchorPrivacyPayload(...) method, but it will not produce a certificate verification URL accepted by /verify.
Successful SDK issuance returns both the chain proof and client artifacts:
transactionHash,dataHash,anchorRecordIdpayloadEncoded,verificationUrl,publicQrUrl,qrPayloadverificationArchiveqrCode.dataUrlwhenincludeQrDataUrlis truepdfBase64whenincludePdfis true
Artifact meaning:
publicQrUrlandqrPayloadare the privacy-preserving QR target. They usehttps://aerocert.co/q/<public-token>#k=<public-key>, then resolve to the public verifier without putting the clear verifier payload in the printed QR.verificationUrlis the self-contained verifier URLhttps://aerocert.co/verify#p=<payloadEncoded>. It contains the public verifier payload in the URL fragment and can be archived by the client system as a long-term recovery path.payloadEncodedis the self-contained verifier payload. Store it with the issued certificate record if your privacy policy allows public certificate verification metadata to be archived outside AeroCert.verificationArchiveis the structured archive object to persist for official certificates. It carries the chain id, transaction hash, data hash, registry contract, organization registry, issuer wallet, organization profile hash, QR URL, self-contained verifier URL, payload, SDK version, and, from0.7.10, the registry version, block number, blockchain timestamp when the AeroCert API response provides them, the organization profile hash algorithm, plus the minimal registry interface signatures needed to decode the archived transaction and reconstruct the on-chain verifier later.
For official mainnet certificates, archive verificationArchive plus the final
issued PDF bytes or PDF hash. When a partner export includes artifact hashes,
the AeroCert archive verifier can also compare local PDFs with
--final-pdf <path> and --aerocert-proof-pdf <path>.
Revocation & Replacement
SDK 0.8.0 adds lifecycle helpers for corrected certificates. Existing issuance
calls are unchanged: certify(...), issuePrivacyCertificate(...), package
exports, ESM imports, and CommonJS imports keep the same contract.
Reason codes are the on-chain CertificateRegistryV4 values:
| Code | Meaning |
|---|---|
| 1 | Error in document |
| 2 | Document expired or replaced |
| 3 | Fraud or falsification |
| 4 | Other |
Use replace(...) when a corrected certificate supersedes an old anchor. The
SDK resolves the replaced anchor dataHash, injects it as
metadata.replaces, injects metadata.correction_reason when reasonText is
provided, anchors the corrected payload, then asks AeroCert to revoke the old
anchor.
import { AeroCert, type ReplaceResponse } from '@aerocert/sdk';
const aerocert = new AeroCert({
apiKey: process.env.AEROCERT_API_KEY!,
apiBaseUrl: 'https://api.aerocert.co',
privacyKey: process.env.AEROCERT_PRIVACY_KEY!,
});
const original = await aerocert.certify({
schema: 'generic:training_certificate',
dataToAnchor: {
certificate_number: 'MBA-2026-0001',
holder_name: 'Alice Moreau',
training_name: 'Aviation Safety',
issued_date: '2026-05-19',
},
});
const replacement: ReplaceResponse = await aerocert.replace({
schema: 'generic:training_certificate',
dataToAnchor: {
certificate_number: 'MBA-2026-0001-CORRECTED',
holder_name: 'Alice Moreau',
training_name: 'Aviation Safety',
issued_date: '2026-05-20',
},
replacesAnchorId: original.anchorId,
reasonCode: 2,
reasonText: 'Corrected score after review',
});
if (replacement.status === 'partial') {
// The corrected certificate exists. Retry revocation of the old anchor only.
await aerocert.revoke({
anchorId: original.anchorId,
reasonCode: 2,
reasonText: 'Corrected score after review',
});
} else {
console.log(replacement.anchorId);
console.log(replacement.lineage.replacesAnchorId);
console.log(replacement.revocation.txHash);
}If your system already loaded the old anchor hash, pass replacesDataHash to
avoid the SDK read of GET /api/v1/privacy/anchors/:id:
const correctedVerifierPayload = {
v: 1,
schema: 'generic:training_certificate',
hashAlgorithm: 'SHA-256',
chainId: 43113,
contractAddress: '0x1111111111111111111111111111111111111111',
issuerOrganizationName: 'Aero Training Org',
issuerWallet: '0x2222222222222222222222222222222222222222',
organizationProfile: { name: 'Aero Training Org' },
organizationProfileHash: '0x3333333333333333333333333333333333333333333333333333333333333333',
recipientName: 'Alice Moreau',
courseName: 'Aviation Safety',
issueDate: '2026-05-20',
certificateNumber: 'MBA-2026-0001-CORRECTED',
metadata: {},
};
await aerocert.replace({
payload: correctedVerifierPayload,
replacesAnchorId: 2475,
replacesDataHash: '0x2424242424242424242424242424242424242424242424242424242424242424',
reasonCode: 2,
reasonText: 'Corrected score after review',
});Webhook receiver signatures
Webhook delivery bodies must be verified from the raw request body:
import { verifyWebhookSignature } from '@aerocert/sdk';
const valid = verifyWebhookSignature({
payload: rawBody,
secret: process.env.AEROCERT_WEBHOOK_SECRET!,
headers: req.headers,
});
if (!valid) {
throw new Error('Invalid AeroCert webhook signature');
}verifyWebhookSignature(...) rejects deliveries whose
X-AeroCert-Webhook-Timestamp is more than 5 minutes from the receiver clock by
default. Keep that check enabled and store processed X-AeroCert-Webhook-Id
values if your handler must be strictly once-only.
The partner contract, endpoint list, and webhook semantics are documented in
docs/api/PARTNER_INTEGRATION_CONTRACT.md.
