Skip to main content
Version: Next

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.

info

When the SDK has no provider configured, it falls back to its local system configuration. This guide only covers the provider-managed case.

Prerequisites

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. userNonce will 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. Use required to decide whether to offer or enforce authentication, and clientId, identityProvider, and redirectUri to 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 to holderActivateInstance.

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.

note

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:

StatusMeaning
PendingRegistered, but the provider expects user authentication and activation before the instance is usable.
ActiveReady to use, whether activation completed or none was required.
RevokedRevoked by the provider.
UnattestedRegistered, 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.
ErrorRegistration 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.