Remote issuance
Remote issuance allows you to issue credentials to recipients who are not present at the time of issuance.
Remote issuance is easier to implement than in-person issuance, since no user-interface is required. If you want control over the issuance process and user interface, use in-person issuance.
Remote issuance requests and their completed credentials are visible together with in-person issuances in the Credentials view in Composer.
Verification options
Described below are the two methods by which the recipient can accept remote issuances:
- Verifying using a one-time passcode (OTP) sent via SMS or email
- Signing in to the Concierge using an existing active credential
Signing in to the Concierge is the recommended method for recipients who already have an active credential. This method is easier for the issuer (verification SMS or email is not required) and provides a good user experience.
OTP verification
The OTP issuance flow is as follows:
- The recipient receives an email or SMS containing a link to accept the credential via the VO Concierge website.
- The recipient opens the link and is verified by an SMS or email as second-factor authentication via an OTP (one-time passcode).
- If the recipient is not verified, the issuance is not completed
- Optionally, a guided photo-capture flow is used to capture a photo of the recipient for credential presentation in combination with Face Check.
- The recipient adds the credential to their wallet.
- On a desktop, the recipient is prompted to scan the QR code to complete the issuance on their mobile device.
- On a mobile device, the wallet is opened via a deep link, and recipient is prompted to save their verifiable credential.
- If the recipient does not have a wallet, they will be prompted to install the Microsoft Authenticator app.
Concierge sign-in verification
The Concierge sign-in issuance flow is as follows:
- The recipient receives an email or SMS containing a link to accept the credential via the VO Concierge website.
- The receipient opens the link, if not already signed-in to the Concierge, they are prompted to sign-in.
- Optionally, a guided photo-capture flow is used to capture a photo of the recipient for credential presentation in combination with Face Check.
- The recipient adds the credential to their wallet.
- On a desktop, the recipient is prompted to scan the QR code to complete the issuance on their mobile device.
- On a mobile device, the wallet is opened via a deep link, and recipient is prompted to save their verifiable credential.
- If the recipient does not have a wallet, they will be prompted to install the Microsoft Authenticator app.
Alternatively, the recipient can sign in to the Concierge at / and see all pending issuances at the top of their home page. They can select each pending issuance to accept the credential.
Notification options
The VO platform can send a notification to the recipient via email or SMS. The recipient will receive a link to complete the issuance.
If you prefer to send your own notification, you can include a link directly for the applicable issuance flow in your own email or SMS (see issuance URLs).
Issuance URLs
Issuance URLs for the two verification methods are as follows, where the $issuanceId is the ID of the remote issuance:
- OTP verification at /issuance/$issuanceId
- Concierge sign-in verification at /issue/$issuanceId
Alternatively, issuees can sign in to the Concierge and see all pending issuances at the top of their home page. They can click or tap on each pending issuance to accept the credential.
Bulk remote issuance
Remote issuance supports bulk credential issuing to many recipients, either via app integration with the VO API or manually via the Composer.
Both the Composer and the API have a hard limit of creating no more than 1000 remote issuances at one time.
- When using the the Composer, you can split the CSV file into multiple files and upload them separately.
- When using the API, you can make multiple requests with a maximum of 1000 remote issuances per request.
Manual remote issuance
The Composer provides the ability to:
- Create remote issuances by clicking 'Issue Credential' when viewing a verifiable credential.
- Create bulk remote issuances from a CSV file by clicking 'Bulk Issue Credential' when viewing a verifiable credential.
- The 'Remote Issuances' section of the Composer allows you to list, filter, cancel, or view the details of remote issuances.
CSV columns
When bulk issuing through The Composer, you will upload CSV files with data arranged into specific columns. There are fixed columns (which are always required) and claim-data columns (which depend on the fields defined in the contract).
Each row represents a single async issuance request and each column corresponds to a field.
| Column | Input mapping | Note |
|---|---|---|
| Identity Identifier | AsyncIssuanceRequestInput.identity.identifier | Unique identifier of the identity (see Identity mapping guide) |
| Identity Issuer | AsyncIssuanceRequestInput.identity.issuer | Issuer of the identity (see Identity mapping guide) |
| Recipient Name | AsyncIssuanceRequestInput.identity.name | Name of the identity (see Identity mapping guide) |
| (Optional) Notification Method (email | sms) | "email" or "sms" | |
| (Optional) Notification Value | Email or phone number for notification of issuance | |
| (Optional) Verification Method (email | sms) | "email" or "sms" | |
| (Optional) Verification Value | Email or phone number for verification of issuance (OTP) | |
| Issuance Expiry (oneDay | oneMonth | oneWeek | threeDays | threeMonths | twoWeeks) | "oneDay" or "threeDays" or "oneWeek" or "twoWeeks" or "oneMonth" or "threeMonths" | |
| (Optional) - Credential Expiry Date | Date-time string in ISO 8601 format | |
| (Optional) - Post issuance redirect url | Redirect to this URL after issuance |
"(Optional)" prefix means the column is not required.
Create remote issuance request
To create a remote issuance request definition and explore the API, use the issuance builder and select 'Remote' as the 'Delivery method' option, as a starting point.
The async issuance request input specifies:
- Which credential will be issued.
- The identity to whom the credential will be issued (recipient / issuee).
- The contact details of the recipient for notifications and verification (both optional) during the issuance flow.
- The values of any claims defined by the contract.
- The expiration of the remote issuance (not the credential), after which the issuance cannot be completed.
- Other optional input:
- A callback URI if issuance event data should also be sent to a server-side endpoint.
- An expiration date for explicit control over when a credential expires, regardless of when it is issued.
- A request to capture a photo of the recipient during issuance if the credential requires a face check photo and the photo is not included with the claim data provided.
- A post issuance redirect url where the user will be redirected to after a successful issuance
PII (personally identifiable information) is deleted after the remote issuance is completed, is cancelled or at expiry.
mutation CreateAsyncIssuanceRequest($request: [AsyncIssuanceRequestInput!]!) {
createAsyncIssuanceRequest(request: $request) {
... on AsyncIssuanceResponse {
asyncIssuanceRequestIds
}
... on AsyncIssuanceErrorResponse {
errors
}
}
}
{
"request": {
"contractId": "<id of the contract to be issued>",
"identityId": "<id of identity of recipient; or specify via identity field>",
"expirationDate": "<explicit expiry date of the credential in ISO format>",
"expiry": "<expiry period of the remote issuance, if not completed>",
"photoCapture": true, // include photo capture in the issuance process to support Face Check
"claims": {
"claimName": "claimValue" // claim data specified by the contract that must be included in the credential
},
"contact": {
"notification": {
"method": "email",
"value": "name@outlook.com"
},
"verification": {
"method": "sms",
"value": "+61412123456"
}
}
}
}
If the async issuance request is successful, the response will contain the async issuance request IDs.
{
"data": {
"createAsyncIssuanceRequest": {
"asyncIssuanceRequestIds": ["2bdb43e8-c85a-48c1-8541-ef634e5dba05", "6372d603-bfe7-4ba8-91d6-771a9b61fb32"]
}
}
}
Handling errors
createAsyncIssuanceRequest returns errors through two mechanically different channels. Read both: a client that only inspects ... on AsyncIssuanceErrorResponse will see an unexplained null result for anything in the second channel.
Per-request validation errors
These are input validation failures against a specific item in the request array. They are returned as AsyncIssuanceErrorResponse.errors, and the mutation still resolves successfully.
Validation is all-or-nothing. If any item in the batch fails validation, no async issuance requests are created — not even for the items that passed. Correct the flagged items and resubmit the whole batch.
The errors array is positional and sparse:
errors[i]corresponds torequest[i], withnullwhere that item had no validation error.- The array can be shorter than the input array. Only entries up to the last failing index are present — trailing items that had no error are omitted, not padded with
null. - A
nullentry means "no validation error was found for this item" — because the batch is atomic, it does not mean that item was created.
{
"data": {
"createAsyncIssuanceRequest": {
"errors": [null, "Claims must include: card_number"]
}
}
}
See reference: per-request validation errors for the full list.
Whole-call errors
Anything thrown outside the per-request validation loop — authorisation, feature-flag checks, the 1000-request cap, Concierge sign-in eligibility, or identity resolution — aborts the entire mutation. data.createAsyncIssuanceRequest is null and neither union member resolves, so these only appear in the top-level GraphQL errors array.
{
"data": {
"createAsyncIssuanceRequest": null
},
"errors": [
{
"message": "Not Authorized!",
"extensions": {
"code": "FORBIDDEN",
"http": { "status": 403 }
}
}
]
}
See reference: whole-call errors for the full list.
Don't treat error message text as a stable, long-term contract — there is no error-code enum on this mutation, and wording may change. Branch on response shape first (AsyncIssuanceErrorResponse vs. top-level errors). Within the top-level errors array, two categories currently carry a stable extensions.code: the authorisation failure uses FORBIDDEN, and GraphQL schema/scalar errors (malformed UUID, DateTime, URL or JSONObject values, and constraint violations such as givenName/surname outside 1–255 characters) use BAD_USER_INPUT. The remaining whole-call errors (the 1000-request cap, feature-flag checks, the batch-IDV restriction, Concierge sign-in eligibility, identity resolution) do not currently expose an equivalent stable code, so their message text is presently the only way to tell them apart.
Errors described here are validation-time only. Failures that occur after the requests are created (recipient rejects, request expires, IDV name mismatch) surface on the async issuance request itself via status, failureReason, and the machine-readable failureReasonCode (rejected, expired, failed, name-mismatch).
Reference: per-request validation errors
Contract and claims
| Error | Cause |
|---|---|
Contract could not be found | contractId doesn't match a contract |
Contract must be provisioned before issuance | Contract exists but isn't published |
Contract must not be deprecated | Contract has been deprecated |
Claims must include: <claims> | Required, non-optional contract claims missing from claims |
Claims must not include any of: issuanceId, name, identityId, photo | Reserved claims supplied |
[Claim Type: <type>] <detail> | A claim value failed the contract's type/validation rules, e.g. length, range, regex, list, date, email, URL, phone, among others |
Unsupported claim type: <type> | Contract declares a claim type that isn't supported |
Recipient and contact details
| Error | Cause |
|---|---|
Identity could not be found | identityId doesn't match an identity |
Email address for contact notification is invalid | Malformed contact.notification.value for email |
Phone number for contact notification must use international E.164 format | Malformed contact.notification.value for SMS |
Email address for contact verification is invalid | As above, legacy contact.verification |
Phone number for contact verification must use international E.164 format | As above, legacy contact.verification |
Verification methods (verificationMethods)
| Error | Cause |
|---|---|
At least one verification method is required | Empty array supplied |
Duplicate verification method: <type> | Same type listed twice |
OTP payload (channel and value) is required when type is otp | otp object omitted |
Email address for OTP verification method is invalid | Malformed OTP email destination |
Phone number for OTP verification method must use international E.164 format | Malformed OTP SMS destination |
IDV payload (givenName, surname) is required when type is idvTrinsic | idvTrinsic object omitted |
idvTrinsic.givenName must not be blank / idvTrinsic.surname must not be blank | Whitespace-only names |
idvTrinsic must be combined with otp or conciergeSignin: IDV alone has no redemption path | IDV specified on its own |
requiresIdentityVerification (v1) is no longer supported for IDV. Use verificationMethods with type idvTrinsic (combined with otp or conciergeSignin) instead. | Legacy IDV flag used |
Photo, face check and expiry
| Error | Cause |
|---|---|
Face check photo or using a photo capture request is required for issuance of this contract | Contract requires face check; neither faceCheckPhoto nor photoCapture supplied |
Contract must support face check when providing either a face check photo or using a photo capture request | Contract doesn't support face check but a photo was supplied |
Face check photo cannot be provided when using a photo capture request | Both faceCheckPhoto and photoCapture supplied |
Face check photo must be a valid image/jpeg data URL with base64 encoding | Photo isn't a base64 image/jpeg data URL |
Credential expiry must fall after the period to claim it | expirationDate falls before the expiry claim window ends |
Payload size
| Error | Cause |
|---|---|
Issuance request payload size exceeds the limit of 1 MB | Single request payload over 1 MB (Microsoft Entra Verified ID limit) |
An unknown error occurred | Fallback for a non-Error throwable — please report this if encountered |
Reference: whole-call errors
| Error | Cause |
|---|---|
Not Authorized! (code FORBIDDEN, HTTP 403) | Token lacks issuer permission — this mutation requires an issuer user role or an issuance application registration |
Cannot create more than 1000 async issuance requests at once | Hard batch cap; use a batching strategy |
Identity verification (IDV) is not supported in batch issuance requests | IDV requested with more than one item in the batch — IDV is single-request only |
Trinsic IDV feature is not available | IDV not enabled on your instance |
Face check feature is not available | Face check not enabled on your instance |
Concierge sign-in verification requires the recipient identity to already hold an active credential | conciergeSignin used for a recipient with no active credential |
Identity or identity ID must be provided / Identity not found | Neither identityId nor identity supplied, or the identity couldn't be resolved |
GraphQL schema/scalar errors (code BAD_USER_INPUT, HTTP 400) | Malformed UUID, DateTime, URL or JSONObject values, or givenName/surname outside 1–255 characters. Returned by the GraphQL layer before this mutation runs |
Remote issuance completion process
Once the remote issuance is created, if notification details are set, the notification email or SMS will be sent to the recipient including a link to complete the issuance.
The issuance link can be opened on a desktop or on a mobile device. Regardless of the method, the recipient will need a mobile device with a wallet or Microsoft Authenticator installed to complete the issuance.
- The recipient opens the issuance link.
- They are prompted either to enter a one-time passcode (OTP) or to sign in to the Concierge.
- If photo capture was requested at time of creation, the recipient will be guided to capture a photo.
- If using a mobile device, the recipient will be prompted to open Microsoft Authenticator.
- If the recipient does not have Microsoft Authenticator installed, they will be prompted to install it.
- Otherwise, the recipient will be prompted to scan the QR code with their mobile device.
- The flow ends with the recipient adding the issued credential to their wallet.
A verifiable credential with Face Check requires presentation via the Microsoft Authenticator app and will not work with other wallet apps.
Administration of remote issuances
Once the remote issuance is created, it can be viewed and managed via the Composer.
- The Remote Issuances page lists all remote issuances, with the ability to filter by status, recipient, credential and date of creation.
- Pending remote issuances can be cancelled by selecting one or more in the list, or from the details page when viewing a single remote issuance.
- The details page shows a communications log, showing issuance and verification communications sent to the recipient. Communication failures are also shown, including the error encountered to assist with troubleshooting.
- The recipient's contact details can be revealed and checked for correctness.
- If required, contact details can be corrected.
- The remote issuance notification can be resent to the recipient.
Testing integration
It's often useful to test the remote issuance flow during integration with your application. However, during testing, you may not want to send real SMS or email notifications to recipients.
Testing with email
The following domains can be used to generate email addresses for testing purposes. These domains will not send emails to real recipients:
@example.com@example.org@example.net@example.edu
Example email addresses that will be ignored: sally.tester@exmple.com, fredrick.tester@example.edu.
Testing with SMS
The following phone numbers can be used for testing purposes. These numbers will not send SMS messages to real recipients:
Australia:
+61491570006+61491570157+61491570737+61491573087+61491578957
United States:
+15005550109+15005550119+15005550129+15005550139+15005550149
United Kingdom:
+447709000018+447709000028+447709000038+447709000048+447709000058