Provider Onboarding
Some deployments run the SDK against a Wallet Provider or Verifier Provider backend, which can provide several features:
- Wallet unit attestations (WUA), including:
- Key attestations
- Instance attestations
- Automated syncing of assets from the provider to managed instances,
including:
- Trust lists
- Credential schemas and proof schemas
- Feature-flag-driven configuration, for example enforcing ecosystem compliance automatically
- Access Certificate provisioning (for verifiers)
- App version enforcement
This guide walks through onboarding an instance with a provider from the SDK side.
When the SDK has no provider configured, it falls back to its local system configuration. This guide only covers the provider-managed case.
Prerequisites
- The SDK is installed and initialized.
- An organization is created.
- You have your provider's URL and type for your environment. You will hardcode your app to register per deployment.
For the concepts behind provider onboarding, including why registration and activation exist as separate steps and how trust is established between your app and the provider, see Wallet Provider and Verifier Provider. For the provider-backend perspective on the same exchange, see Wallet Provisioning Flows and Verifier Provisioning Flows.
Overview
All managed instances must register with a provider. If the provider is configured to require user authentication, the instance must also complete authentication and activation.
1. Register the instance
Registration establishes your app's identity with the provider and returns an instance ID for all future calls.
const registration = await core.holderRegisterInstance({
organisationId: organisationId,
role: 'WALLET', // or 'VERIFIER'
provider: {
url: 'https://provider.example.com/ssi/wallet-provider/v1/PROCIVIS_ONE',
type: 'PROCIVIS_ONE',
},
keyType: 'ECDSA',
});
const { id: instanceId, status, userNonce } = registration;
provider.url and provider.type are your own hardcoded values,
typically one per environment. provider.url is the full URL to the
provider's GET metadata endpoint. For a wallet provider this looks
like <domain>/ssi/wallet-provider/v1/<provider>, and for a verifier
provider, <domain>/ssi/verifier-provider/v1/<provider>. type is
just PROCIVIS_ONE.
The system generates a dedicated key for registration, so you do not
need to call generateKey yourself first.
status tells you whether anything further is required:
ACTIVE: Your instance is registered and ready to use immediately. If the provider configured Wallet Unit Attestations (including Key Attestation and/or Instance Attestation), these attestations are available from the provider and will be requested and fetched automatically during issuance flows. See Confirm registration state below for how to poll your instance status going forward.PENDING: the provider expects you to authenticate the user and then activate the instance. Continue to Optional: authenticate and activate below.userNoncewill be present in this case; hold onto it, you need it for activation.
During this same call, Core also fetches ecosystems and
featureFlags from the provider's endpoint. Ecosystems get set up for
automated syncing. Feature flags can go further: when a flag conflicts
with your organization's local configuration, Core overrides the local
setting to comply. For example, if the provider has
ecosystemsEnforcementEnabled turned on, Core turns on enforcement
for your organization during registration, regardless of what your
local config says.
When your provider requires app integrity verification, Core also generates the platform attestation (iOS App Attest or Android Key Attestation) during this same registration call, using the device's Secure Enclave or Keystore under the hood. There is nothing to wire up natively for this; it works out of the box in the React Native package. This happens here regardless of whether user authentication is also required.
keyType also appears in the activation call below, but only one key
pair is ever generated. In every configuration except one, the key
type from this registration call is the one actually used. Only when
both user authentication and app integrity verification are required
does the key type from the activation call end up being used instead.
In practice, using the same value for both calls avoids having to
think about which case you are in.
2. Fetch the provider's configuration directly
Beyond what Core reads for its own purposes during registration (see step 1), the full provider response is available for your app to build against directly: app version prompts, feature-gated screens, anything you want your app's behavior to vary on. Fetch it with your own networking code using a plain GET to the provider URL from step 1. Fetch it fresh on every app startup rather than caching it indefinitely, since values here can change between sessions.
GET https://provider.example.com/ssi/wallet-provider/v1/PROCIVIS_ONE
{
"appVersion": {
"minimum": "1.4.0",
"minimumRecommended": "1.6.0",
"reject": ["1.0.0", "1.1.0"],
"updateScreen": { "link": "https://example.com/update" }
},
"userAuthentication": {
"clientId": "your-client-id",
"identityProvider": "https://idp.example.com",
"redirectUri": "yourapp://callback",
"required": true,
"tokenValidation": {
"aud": "your-client-id",
"iss": "https://idp.example.com",
"jwksUri": "https://idp.example.com/.well-known/jwks.json"
}
},
"featureFlags": {
"trustEcosystemsEnabled": true,
"ecosystemsEnabled": true,
"refreshCredentialBatchEnabled": false,
"documentSigningEnabled": true,
"ecosystemsEnforcementEnabled": false
},
"documentSigners": [
{
"name": "SIGN8",
"type": "WALLET_CENTRIC",
"displayName": [
{
"lang": "de",
"value": "Sign8"
},
{
"lang": "en",
"value": "Sign8"
}
],
"description": [
{
"lang": "de",
"value": "External (Wallet centric)"
},
{
"lang": "en",
"value": "External (Wallet centric)"
}
],
"logo": "data:image/png;base64,..."
}
]
}
Two fields are directly relevant to this guide:
appVersion, for update gating.userAuthentication, if you want to support the optional flow in step 3. Userequiredto decide whether to offer or enforce authentication, andclientId,identityProvider, andredirectUrito configure your own OAuth library. Your app needs to fetch this itself, use it to run its own OAuth flow, and pass the resulting token toholderActivateInstance.
featureFlags is also here. Core reads some of these itself during
registration to configure your organization automatically (see step
1); your app can also read the same block directly, for example to
gate its own screens. The same flags are available after registration
through
Check the organization-level summary
too, if you would rather read them from there.
Beyond those, build against whatever else is useful to your app.
3. Optional: authenticate and activate
This section only applies if registration returned PENDING. If you
got ACTIVE back, your instance is fully onboarded. Skip ahead to
Confirm registration state.
3.1 Authenticate with your identity provider
Authentication happens entirely outside the SDK. Your app runs its own
OAuth flow against the identity provider described in
Fetch the provider's configuration directly
above, passing along the userNonce from registration per the
provider's requirements, and receives back a user ID token and access
token.
See the user authentication sections of Providing Wallets and Providing Verifiers for what the provider expects in this exchange.
3.2 Activate the instance
const activation = await core.holderActivateInstance(instanceId, {
keyType: 'ECDSA',
userIdToken: idToken,
userAccessToken: accessToken,
});
This call submits your user's authentication tokens to the provider.
For a verifier instance, Core also builds the certificate signing
request and completes Access Certificate issuance in the same call.
There is no separate CSR-subject input here. That is a distinct feature
(CSRSubject, used for general-purpose identifier CSRs) and isn't
part of this flow; the certificate's subject fields come from your
provider's own
Access Certificate configuration,
not from the client.
Wallet and Verifier instances diverge slightly here. A Wallet
instance is simply confirmed active, and nothing further is returned.
A Verifier instance additionally receives
accessCertificateIdentifierId, referencing the Access Certificate
the provider issued during this call.
3.3 Retrieve the Access Certificate (verifiers only)
The Access Certificate is not returned inline. Activation gives you an identifier ID that resolves to it:
if (activation.accessCertificateIdentifierId) {
const identifier = await core.getIdentifier(
activation.accessCertificateIdentifierId
);
// identifier.certificates contains the issued Access Certificate
}
This gives you the certificate itself, but not a ready-to-use
identifier. Assembling it into a full identifier, for example alongside
a Registration Certificate as part of trustInformation, is a
separate, non-trivial step that this guide doesn't cover.
4. Confirm registration state
Once registration (and activation, if needed) is complete, checking
status is essentially the only ongoing interaction your app has with
the provider (see Next steps). It takes two calls, not
one. holderInstanceStatus asks the provider to re-evaluate the
instance: in a sense it's requesting an instance attestation, though
you never receive the attestation itself. A successful call only
returns a 204, which tells you the request went through, not what your
status actually is. The status itself only shows up in
holderGetInstance's response, so call that right after:
// Ask the provider to re-evaluate this instance's status.
// A successful call returns nothing useful on its own.
await core.holderInstanceStatus(instanceId);
// The actual status lives here.
const details = await core.holderGetInstance(instanceId);
const { status } = details;
A failed attestation request elsewhere is not a reliable substitute for this check. For example, if an instance has been revoked and a credential issuance flow tries to request an attestation, the issuance simply fails; it does not refresh or surface the underlying instance status on its own. Call the sequence above directly, and on a regular basis, rather than inferring status from unrelated failures.
This mechanism may change as the WUA implementation is updated to align with the EU ARF changes.
status is one of:
| Status | Meaning |
|---|---|
Pending | Registered, but the provider expects user authentication and activation before the instance is usable. |
Active | Ready to use, whether activation completed or none was required. |
Revoked | Revoked by the provider. |
Unattested | Registered, but the instance did not pass the integrity or key security check. Attestations are not available, but the instance still exists on the provider and can do things like sync ecosystems. |
Error | Registration or activation failed. |
There is no deleteInstance or revokeInstance call on this side.
Revoking or deleting an instance is a provider-side administrative
action, not something the app initiates. See
Lifecycle management.
5. Check the organization-level summary
Once registered, the instance also shows up on the organization itself:
const org = await core.getOrganisation(organisationId);
console.log(org.walletInstance); // or org.verifierInstance
This includes the provider URL/name, the authentication key type, and
the feature flags the provider has advertised for this instance, for
example ecosystemsEnabled or, for verifiers,
accessCertificateProvisioningEnabled. These reflect configuration
Core already applied during registration (see step 1). Core, not your
app, is what acts on them, for example by overriding your
organization's local config to comply with an enforced ecosystem.
Ecosystem selection and enforcement is covered separately in
Enforcing an Ecosystem as a Provider.
Trust collections referenced by an ecosystem are resolved by Core
internally, through a GET /ssi/trust-collection/v1/{id} call.
There is nothing to do on the app side for this.
Next steps
This covers registering and activating an instance. From here:
- Wallet and Key Attestations (WIA/KA) are requested automatically during normal wallet workflow, when handling an invitation and accepting a credential, once your instance is active. See Wallet Provisioning Flows. There's nothing further to set up for this on the SDK side.
- Enforcing an Ecosystem as a Provider covers selecting ecosystems and enabling trust and schema syncing once registered.
- Wallet Workflow covers issuance and presentation, which work the same way once an instance is active as they do without a provider.