Print credential from a template
With this quick start guide, we'll print our first Document Rails credential with a .docx template or a PDF form.
This quick start guide assumes that you already have an account with required privileges to create resources.
Other Document Rails resources are optional, as we are going to create them step-by-step in this tutorial.
Creating a new organization
In Document Rails, organizations are utilized to segment resources between different units or projects within your company. For example, you may create separate organizations for "Digital pass validation" and "Conformance certificate issuance" and assign different users to them with different permissions, improving data access control and security.
First, let's authenticate with Document Rails. For production, we recommend utilizing service account authentication, but for tutorial simplicity, let's use the password-based method.
Reading code examples
Code examples in this tutorial are split for brevity, but you can combine different code blocks into a single file for testing.
import { loginWithPassword, ImpressumClient } from "@vaultie/document-rails";
const client = new ImpressumClient();
const accessToken = await loginWithPassword(client, {
email: "...",
password: "...",
});
Replace email and password placeholders with values corresponding to one of your tenancy user accounts.
Then, create an organization named "Printing tutorial". We are going to utilize it throughout this tutorial.
import { createOrganization } from "@vaultie/document-rails";
const { id: organizationId } = await createOrganization(client, accessToken, {
name: "Printing tutorial",
// Viewer URL to utilize when embedding QR code credentials.
qr_code_base_url: "https://viewer.vaultie.io"
});
Creating signing keys
Digital credentials are cryptographically signed with signing keys. To manage signing keys, Document Rails features a separate API, that allows you to utilize entirely different private key sources using the same codebase.
For this example, let's upload a locally generated private key to Document Rails. For production, we recommend setting up a remote key service, such as Azure Key Vault, or Google Cloud KMS.
To sign C2PA documents, we need to also create a certificate authority. For this tutorial, we will create a root certificate authority, and an end-entity certificate derived from it.
To generate a root ECDSA key with OpenSSL, you may run the following command locally:
openssl genpkey \
-algorithm ec \
-pkeyopt ec_paramgen_curve:prime256v1 \
-out root.pem
Next, let's create a new root signing key using the API.
import { readFile } from "node:fs/promises";
import {
createCertificateAuthority,
createKey,
getCertificate,
CreateCertificateAuthorityRequestOriginType,
ExtendedKeyUsage,
KeyType,
KeyUsage,
} from "@vaultie/document-rails";
const rootKey = await readFile("root.pem", { encoding: "utf8" });
const { id: rootSigningKeyId } = await createKey(
client,
accessToken,
organizationId,
{
key: {
// We create a local key here, but other key types are also available.
type: KeyType.Local,
pem: rootKey,
},
}
);
const {
id: rootCertificateAuthorityId,
certificate_id: rootCertificateId,
} = await createCertificateAuthority(
client,
accessToken,
organizationId, {
type: CreateCertificateAuthorityRequestOriginType.SelfSigned,
key_id: rootSigningKeyId,
common_name: {
common_name: "Example Root CA",
organization_name: "Example",
organization_unit_name: "Development",
country_name: "XX",
state_name: "XX",
},
constraints: {
key_usage: [KeyUsage.DigitalSignature, KeyUsage.KeyCertSign]
},
}
);
// Fetch the root certificate PEM, we'll need it later to form a certificate chain.
const { certificate: rootCertificateData } = await getCertificate(
client,
accessToken,
organizationId,
rootCertificateId,
);
After the root certificate authority is initialized, we may generate an end-entity key locally:
openssl genpkey \
-algorithm ec \
-pkeyopt ec_paramgen_curve:prime256v1 \
-out end-entity.pem
With that, we can proceed with initialization of the end-entity signing key and certificate issuance.
For this tutorial, we plan to issue an SD-JWT credential, so we also need to specify the issuer value, that will be included as the iss value of the resulting credential.
The iss value itself is embedded as part of the SD-JWT credential, and is used to uniquely identify the credential issuer for other tools within the digital credential ecosystem.
import { issueWithCertificateAuthority } from "@vaultie/document-rails";
const endEntityKey = await readFile("end-entity.pem", { encoding: "utf8" });
const { id: signingKeyId } = await createKey(
client,
accessToken,
organizationId,
{
key: {
type: KeyType.Local,
pem: endEntityKey
},
issuer: "https://example.com/sdJwtIssuer"
},
);
const { pem: certificate } = await issueWithCertificateAuthority(
client,
accessToken,
organizationId,
rootCertificateAuthorityId,
{
key_id: signingKeyId,
common_name: {
common_name: "Example certificate",
organization_name: "Example",
organization_unit_name: "Development",
country_name: "XX",
state_name: "XX",
},
constraints: {
key_usage: [KeyUsage.DigitalSignature],
extended_key_usage: [
ExtendedKeyUsage.Critical,
ExtendedKeyUsage.EmailProtection
],
},
},
);
Since now we have the certificate value, we may update our signing key with a valid certificate chain that will be embedded into new issued documents.
import { updateKey, CertificateType } from "@vaultie/document-rails";
await updateKey(client, accessToken, organizationId, signingKeyId, {
certificate: {
type: CertificateType.Local,
pem: certificate,
},
});
In production, you may have different signing keys for different purposes. It is also a good practice to periodically rotate signing keys for security purposes.
Configure credentials
In Document Rails, credentials are pre-configured before usage, as the issuance itself depends on the credential kind, attached contexts, variable types, etc.
Usually, you would do this just once for a particular organization, as you can re-use existing credential configurations in different scenarios.
For this tutorial, let's assume that we want to issue an event pass.
All credentials are identified by their credential kind and type.
Credential kind is used to distinguish between different specifications and representations of digital credentials. For example, W3C and SD-JWT credentials have their own credential kind values. In this example, we'll utilize SD-JWT credentials, so we select that credential kind as well.
Credential type is a unique value that is used to identify the credential itself, its fields, behavior, etc. For this example, we'll use the credential type value of https://example.com/EventPass.
To represent data within issued credentials, we specify the variables field, which contains descriptors about the supported credential claims and their types. For now, let's assume that our event pass is going to have the event name and visitor's first and last name.
import { createCredential, CredentialVarType, StringifiedCredentialKind } from "@vaultie/document-rails";
const { id: eventPassCredentialId } = await createCredential(
client,
accessToken,
organizationId,
{
credential_kind: StringifiedCredentialKind.SDJWT,
credential_type: "https://example.com/EventPass",
variables: {
event_name: CredentialVarType.String,
first_name: CredentialVarType.String,
last_name: CredentialVarType.String,
},
},
);
With that code fragment, we created a new output credential, but Document Rails also has a concept of input credentials. Input credentials are utilized to get the data into issuance flow, serve as a source for output credentials data.
Usually, you would utilize "plain data credentials", which are just JSON objects without any special requirements. However, it's also possible to use other issued credentials as a source for newly issued credentials. This way, you "link" multiple different credentials by sourcing new ones from existing ones.
Let's create a new input credential with type EventPassData and plain data kind.
const { id: inputCredentialId } = await createCredential(
client,
accessToken,
organizationId,
{
credential_kind: StringifiedCredentialKind.PlainData,
credential_type: "EventPassData",
variables: {
event_name: CredentialVarType.String,
first_name: CredentialVarType.String,
last_name: CredentialVarType.String,
},
},
);
You may mirror output credential fields when creating input credentials, just like in this tutorial. Or, you may create multiple input credentials to source output credential fields from different inputs. The workflow is highly flexible as Document Rails strives to support as many different scenarios as possible.
Uploading a template
For this tutorial, you may choose to utilize either .docx files or PDF forms.
.docx templates
You may skip the creation step by downloading existing template instead.
Creating a template from scratch
For convenience, you may install the Document Rails extension from Microsoft AppSource.
Create a new empty document with titled "Event pass". Change the page size to a suitable value. For example, we'll use 3" x 4".

Next, we'll add some information about the user to the page. Document Rails identifies placeholder values using a special curly-bracket syntax.
The extension can help you with converting existing texts to placeholders. Right-click on the selected text and select the "Add Document Rails placeholder" option.

You should see something similar to that:

After that, let's add a placeholder image for embedding QR code credentials.
Select the "Document Rails" tab and click on the "Insert credential QR code placeholder" option.
Position the image similar to the following example:

Document Rails will automatically replace the image placeholder with the generated QR code where applicable.
Uploading a .docx file
To upload an existing .docx file to Document Rails, use the following code sample:
import { createTemplate, TemplateType } from "@vaultie/document-rails";
const { id: templateId } = await createTemplate(
client,
accessToken,
organizationId,
{
template_type: TemplateType.Docx,
file: new Blob([await readFile("Event pass.docx")]),
},
);
For .docx templates, Document Rails automatically detects template variables. You can verify that with the following call:
import { getTemplate } from "@vaultie/document-rails";
const { template_vars } = await getTemplate(
client,
accessToken,
organizationId,
templateId
);
console.log(`Detected template variables: ${JSON.stringify(template_vars)}`);
Creating PDF forms
We recommend using Adobe® Acrobat® for creating PDF forms.
Creating a form from scratch
Create a new empty form by opening the "File" menu tab:

Form editing tools should appear on the sidebar:

Supported components
Document Rails supports text and image field components.
Select "Text field", your cursor should change to a text field placeholder:

Click on the page to insert the text field into the document, right-click on the field and select "Properties":

Edit the text field name to make the field recognizable by Document Rails:

Create text fields with the following names to continue:
-
first_name -
last_name -
event_name
After that, we can create an image field to insert the credential QR code.
Select "Image field", your cursor should change to an image field placeholder:

Click on the page to insert the image field into the document, right-click on the field and select "Properties":

Edit the image field name to make it recognizable by Document Rails. In our case, to make QR code credentials functional, we need to utilize the __impressum_qrcode name:

Uploading a PDF form
To upload your PDF form to Document Rails, use the following code sample:
import { createTemplate, TemplateType, TemplateVarType } from "@vaultie/document-rails";
const { id: templateId } = await createTemplate(
client,
accessToken,
organizationId,
{
template_type: TemplateType.Pdf,
file: new Blob([await readFile("Event pass.pdf")]),
template_vars: [
{ name: "first_name", type: TemplateVarType.String },
{ name: "last_name", type: TemplateVarType.String },
{ name: "event_name", type: TemplateVarType.String }
]
},
);
We previously declared all template variables. You can verify that with the following call:
import { getTemplate } from "@vaultie/document-rails";
const { template_vars } = await getTemplate(
client,
accessToken,
organizationId,
templateId
);
console.log(`Detected template variables: ${JSON.stringify(template_vars)}`);
Creating an outbound webhook
Issuance process in Document Rails is asynchronous by design, meaning that you submit a task and some time later get the result.
To submit the final result of the issuance process, Document Rails utilizes outbound webhooks, which accept regular HTTP requests with the information about a specific request.
If your application can handle a request from a public network, most likely you can integrate outbound webhook protocol support directly into it.
Outbound webhooks utilize CBOR for encoding the request.
For tutorial purposes, we can utilize the following Express server snippet as the example outbound webhook server.
import { decode } from "cbor2";
import express from "express";
const webhookApp = express();
webhookApp.post(
"/outbound",
// Outbound webhooks accept CBOR requests.
express.raw({ type: "*/*" }),
async (req, res) => {
// Decode the incoming request bytes as CBOR.
const request = decode(req.body);
if (request.outcome === "success") {
res.status(200);
console.log(`Success response: ${JSON.stringify(request)}`)
} else {
res.status(400);
console.error(`Invalid response: ${JSON.stringify(request)}`);
}
res.send({});
},
);
webhookApp.listen(8080);
To add information about the outbound webhook to Document Rails, we can utilize the following code snippet:
import { createOutboundWebhook } from "@vaultie/document-rails";
const { id: outboundWebhookId } = await createOutboundWebhook(
client,
accessToken,
organizationId,
{
// The URL should point to your application's outbound webhook handler.
url: "https://example.com/outbound",
}
);
Assembling a recipe
Document Rails recipes are used to describe the issuance flow - input and output credentials, used templates, preferred signing keys, etc.
In this tutorial, we'll just print a PDF with a QR code credential in it.
When mapping variables (credential to credential or credential to template) you need to know their identifiers.
So, first, we have to fetch the up-to-date information about our input credential, output credential, and the template. Then, we'll use SDK utility functions to transform their variables into an object, suitable for querying by the entity name.
This step is technically optional, as you may implement your own mechanism to get credential and template variable identifiers.
import { getCredential, credentialVariablesToObject, templateVariablesToObject } from "@vaultie/document-rails";
const { credential_vars: inputCredentialVars } = await getCredential(
client,
accessToken,
organizationId,
inputCredentialId
);
const mappedInputCredentialVars = credentialVariablesToObject(inputCredentialVars);
const { credential_vars: eventPassCredentialVars } = await getCredential(
client,
accessToken,
organizationId,
eventPassCredentialId
);
const mappedEventPassCredentialVars = credentialVariablesToObject(eventPassCredentialVars);
// We previously created the template_vars variable.
const mappedTemplateVars = templateVariablesToObject(template_vars);
With that settled, let's create the recipe itself:
import { createRecipe } from "@vaultie/document-rails";
const { id: recipeId } = await createRecipe(
client,
accessToken,
organizationId,
{
template_id: templateId,
templates: {
// Here, we map input credential variables to an array of template variables.
// Arrays are used to allow mapping the same input credential variable to multiple template variables.
//
// In our case, the mapping is fairly straightforward.
//
// Also, notice how here we refer to variables using the regular object access syntax.
[mappedInputCredentialVars.first_name]: [mappedTemplateVars.first_name],
[mappedInputCredentialVars.last_name]: [mappedTemplateVars.last_name],
[mappedInputCredentialVars.event_name]: [mappedTemplateVars.event_name],
},
output_credentials: {
// Same as before, map variables 1:1, but this time for the output credential.
[mappedInputCredentialVars.first_name]: [mappedEventPassCredentialVars.first_name],
[mappedInputCredentialVars.last_name]: [mappedEventPassCredentialVars.last_name],
[mappedInputCredentialVars.event_name]: [mappedEventPassCredentialVars.event_name],
},
outbound_webhooks: [outboundWebhookId],
// For credential to be converted to its QR code form we have to designate it as a "QR code credential"
qr_code_credential_id: eventPassCredentialId,
},
);
Recipes are re-usable, usually you would create a few recipes for desired issuance scenarios and re-use them after that.
Printing the credential
Onto the last step - printing the credential.
To print a credential, we have to specify an identifier of the existing recipe, signing keys to use for this transaction, and any input credentials we want to utilize.
Document Rails is highly flexible, which means that sometimes you may omit or add certain fields depending on your use case.
Since recipe printing is asynchronous, we won't receive the result immediately. Instead, Document Rails will provide the unique request identifier, which can be used in conjunction with the outbound webhook to track and save issuance results.
import { printRecipe } from "@vaultie/document-rails";
const requestId = await printRecipe(
client,
accessToken,
organizationId,
recipeId,
{
issuer_configurations: {
c2pa: signingKeyId,
jwt: signingKeyId
},
credentials: {
// Provide the input credential.
// Since we configured it to be plain data, here it's just a JSON object.
[inputCredentialId]: {
first_name: "John",
last_name: "Doe",
event_name: "Example Event 2025"
},
},
credential_subject_ids: {
// Use a random JWK as a credential subject identifier.
//
// In production, for paper credentials you would usually utilize either W3C credential kind,
// which accepts `null` credential subject identifiers. For mobile credentials, exchange protocols
// pass user's public key within the initial credential issuance request.
[eventPassCredentialId]: {
kty: "EC",
use: "sig",
crv: "P-256",
x: "VFDgYhkrl4zghGTTnKo54ZsjJN3MRYLQcasp3toJZLk",
y: "fcJGhY735LcHvCJv_KlJLDI4G50DN68CKN9yjCRFEmI",
alg: "ES256"
}
},
output_credential_ids: {
// Generate a random UUID as the credential identifier.
//
// In production, credential identifiers should be stored in a database.
[eventPassCredentialId]: crypto.randomUUID(),
}
}
);
console.log(`Request ID: ${requestId}`);
After some time, your outbound webhook should receive the issuance outcome.
Legal notices
Adobe, Acrobat are either registered trademarks or trademarks of Adobe in the United States and/or other countries. Other product and company names mentioned herein may be trademarks of their respective owners.