Skip to main content

API Integration for Authentic Sources

This guide covers integrating with the SIROS ID Issuer using REST and gRPC APIs—ideal for organizations where credential data comes from authoritative systems rather than identity providers.

When to Use API Integration​

Choose API integration when:

  • Authentic source systems (HR databases, student information systems, government registries) need to push data directly
  • Batch issuance is required for large-scale credential provisioning
  • Pre-authorized flows enable users to collect credentials without additional authentication
  • Custom identity matching logic is needed beyond standard IAM attributes
  • Non-IAM workflows where the user doesn't authenticate via SAML/OIDC
Comparison
Integration TypeBest ForUser Auth
SAML IdPFederation, existing SSOSAML assertion
OIDC ProviderModern IdP, claims-basedID token
API IntegrationBackend systems, batch, automationPre-authorized or PID-based

Architecture Overview​

API integration uses the Datastore API for document management and the Issuer gRPC API for credential signing:

Datastore REST API​

The Datastore API allows authentic sources to upload documents, manage identities, and trigger credential issuance flows.

Base URL​

https://issuer.example.org/api/v1

Authentication​

The /api/v1 endpoints authenticate with a JWT Bearer token. There are two ways to configure key discovery, and they are mutually exclusive — enable at most one:

  • Static JWKS (api_auth.jwks): tokens are validated against a JWKS URL or a local JWKS file you name.
  • OIDC discovery (api_auth.oidc): the JWKS endpoint is discovered from the provider's .well-known/openid-configuration. The same block's RP fields also drive the admin UI login flow.

In both cases requests carry an Authorization: Bearer <JWT> header, and optional SPOCP rules add fine-grained per-endpoint authorization.

caution

If neither jwks nor oidc is enabled, no authentication is applied and the /api/v1 route group is open. There is no HTTP Basic Auth option for these endpoints. Always enable one of the two in any deployment reachable from outside the host.

Endpoint Summary​

The API is organized into two resource groups under /api/v1:

Datastore Endpoints (/api/v1/datastore)​

MethodEndpointDescription
POST/datastoreUpload a document
POST/datastore/bulkBulk upload multiple documents
GET/datastoreGet document by key (query params)
PUT/datastoreReplace an existing document
DELETE/datastoreDelete a document (body)
POST/datastore/listList documents for an identity
POST/datastore/resolveResolve identity attributes to documents
GET/datastore/searchSearch documents
PUT/datastore/identityAdd identity mapping IDs to a document
DELETE/datastore/identityRemove identity mapping from a document
POST/datastore/preauth_offerCreate a pre-authorized credential offer for a document

Identity Mapping Endpoints (/api/v1/identity/mapping)​

MethodEndpointDescription
POST/identity/mappingCreate an identity mapping
POST/identity/mapping/bulkBulk create identity mappings
POST/identity/mapping/resolveResolve attributes → person ID
PUT/identity/mappingUpdate an identity mapping
DELETE/identity/mappingDelete an identity mapping
GET/identity/mapping/searchSearch identity mappings

Identity Mapping​

Identity mappings link external identity attributes (name, birth date, etc.) to an internal authentic_source_person_id. Documents reference these IDs to establish who can collect a credential.

Create an Identity Mapping​

POST /api/v1/identity/mapping
Content-Type: application/json
Authorization: Bearer <your-jwt-token>

{
"authentic_source": "hr.example.org",
"authentic_source_person_id": "EMP-12345",
"attributes": {
"family_name": "Smith",
"given_name": "Alice",
"birth_date": "1990-05-15"
}
}

Response:

{
"authentic_source_person_id": "EMP-12345"
}

If authentic_source_person_id is omitted, a UUIDv7 is generated automatically.

Resolve Attributes to a Person ID​

When a user presents their PID credential, resolve their identity to an internal person ID:

POST /api/v1/identity/mapping/resolve
Content-Type: application/json

{
"authentic_source": "hr.example.org",
"attributes": {
"family_name": "Smith",
"given_name": "Alice",
"birth_date": "1990-05-15"
}
}

Response:

{
"authentic_source_person_id": "EMP-12345"
}

Bulk Create Mappings​

POST /api/v1/identity/mapping/bulk
Content-Type: application/json

{
"mappings": {
"emp1": {
"authentic_source": "hr.example.org",
"authentic_source_person_id": "EMP-001",
"attributes": { "family_name": "Smith", "given_name": "Alice", "birth_date": "1990-05-15" }
},
"emp2": {
"authentic_source": "hr.example.org",
"authentic_source_person_id": "EMP-002",
"attributes": { "family_name": "Jones", "given_name": "Bob", "birth_date": "1985-03-22" }
}
}
}

Response: { "count": 2 }

# Update attributes
PUT /api/v1/identity/mapping
{ "authentic_source": "hr.example.org", "authentic_source_person_id": "EMP-12345", "attributes": { "family_name": "Smith-Jones" } }

# Delete
DELETE /api/v1/identity/mapping
{ "authentic_source": "hr.example.org", "authentic_source_person_id": "EMP-12345" }

# Search (query params)
GET /api/v1/identity/mapping/search?authentic_source=hr.example.org&search=Smith&limit=50

Document Upload Flow​

Step 1: Upload Document Data​

Upload the credential data before the user requests it. Documents reference identity mappings by their authentic_source_person_id:

POST /api/v1/datastore
Content-Type: application/json
Authorization: Bearer <your-jwt-token>

{
"meta": {
"authentic_source": "hr.example.org",
"scope": "diploma",
"document_id": "diploma-2025-001234"
},
"identity_mapping_ids": ["EMP-12345"],
"document_data": {
"degree_type": "Bachelor of Science",
"field_of_study": "Computer Science",
"issuing_institution": "Example University",
"graduation_date": "2025-05-15",
"gpa": "3.8",
"credits_earned": 120
}
}

Response:

{
"document_id": "diploma-2025-001234"
}

If document_id is omitted from meta, a UUIDv7 is generated automatically.

Step 2: Generate Credential Offer​

Navigate to the credential offer UI or use the API to create an offer for the user:

GET /offers/<scope>

The route is wallet-independent — the offer is generated once and the response (or the rendered page, for a browser) carries all three renderings: the QR code, the deep link, and a per-wallet redirect URI for each wallet configured in apigw.delivery.credential_offers.wallets. There is no /offers/<scope>/<wallet_id> form; a wallet is not selected before the offer is created.

The user scans the QR code or clicks the deep link (or a wallet-specific redirect) to initiate credential collection in their wallet.

Step 3: Notify the User​

Send the credential offer link to the user via:

  • Email notification
  • Portal display
  • SMS with deep link
  • In-app notification

Document Management​

Get a Document by Key​

GET /api/v1/datastore?authentic_source=hr.example.org&scope=diploma&document_id=diploma-2025-001234
Authorization: Bearer <your-jwt-token>

List Documents for an Identity​

POST /api/v1/datastore/list
Content-Type: application/json

{
"authentic_source": "hr.example.org",
"identity_mapping_id": "EMP-12345",
"scope": "diploma"
}

Resolve Identity Attributes to Documents​

POST /api/v1/datastore/resolve
Content-Type: application/json

{
"authentic_source": "hr.example.org",
"attributes": {
"family_name": "Smith",
"given_name": "Alice",
"birth_date": "1990-05-15"
},
"scope": "diploma"
}

Replace a Document​

PUT /api/v1/datastore
Content-Type: application/json

{
"meta": {
"authentic_source": "hr.example.org",
"scope": "diploma",
"document_id": "diploma-2025-001234"
},
"identity_mapping_ids": ["EMP-12345"],
"document_data": {
"degree_type": "Bachelor of Science",
"field_of_study": "Computer Science",
"gpa": "3.9"
}
}

Delete a Document​

DELETE /api/v1/datastore
Content-Type: application/json

{
"authentic_source": "hr.example.org",
"scope": "diploma",
"document_id": "diploma-2025-001234"
}

Add/Remove Identity Mappings on a Document​

# Add identities to an existing document
PUT /api/v1/datastore/identity
Content-Type: application/json

{
"authentic_source": "hr.example.org",
"scope": "diploma",
"document_id": "diploma-2025-001234",
"identity_mapping_ids": ["EMP-67890"]
}

# Remove an identity from a document
DELETE /api/v1/datastore/identity
Content-Type: application/json

{
"authentic_source": "hr.example.org",
"scope": "diploma",
"document_id": "diploma-2025-001234",
"identity_mapping_ids": ["EMP-67890"]
}

Search Documents​

GET /api/v1/datastore/search?authentic_source=hr.example.org&scope=diploma&search=Smith&limit=50

Pre-Authorized Code Flow​

For server-to-server issuance where the user has already been authenticated by your system:

Configuration​

The pre-authorized code flow needs no switch to turn on — creating an offer with POST /api/v1/datastore/preauth_offer is what enables it. The only setting is whether each offer carries a transaction code (PIN) the wallet must present at the token endpoint:

apigw:
auth_providers:
preauth:
enable_pin: false

Credential Offer Format​

The offer carries a pre-authorized code grant. credential_configuration_ids holds the credential scope (here diploma), and the offer is valid for 5 minutes. The tx_code object is present only when enable_pin is on:

{
"credential_issuer": "https://issuer.example.org",
"credential_configuration_ids": ["diploma"],
"grants": {
"urn:ietf:params:oauth:grant-type:pre-authorized_code": {
"pre-authorized_code": "oaKazRN8I0IbtZ...",
"tx_code": {
"input_mode": "numeric",
"length": 6,
"description": "Enter the PIN provided by the issuer"
}
}
}
}

Revocation​

Credential status is published as Token Status Lists by the registry service. Deleting a document through DELETE /api/v1/datastore removes the stored document so no further credentials can be issued from it, but it does not change the status of credentials that were already issued: apigw never calls the registry to update a status.

To revoke an issued credential, update its status entry in the registry, either through the registry admin GUI (registry.admin_gui, POST /admin/status) or by calling the registry's TokenStatusListUpdateStatus gRPC method directly. Verifiers checking the Token Status List then see the credential as revoked.


gRPC Integration​

For advanced integrations, the Issuer Service exposes a gRPC API for direct credential signing.

Service Definition​

service IssuerService {
rpc MakeSDJWT (MakeSDJWTRequest) returns (MakeSDJWTReply) {}
rpc MakeMDoc (MakeMDocRequest) returns (MakeMDocReply) {}
rpc MakeVC20 (MakeVC20Request) returns (MakeVC20Reply) {}
rpc MakeJWP (MakeJWPRequest) returns (MakeJWPReply) {}
rpc JWKS (Empty) returns (JwksReply) {}
rpc SignMetadata (SignMetadataRequest) returns (SignMetadataReply) {}
rpc GetIACAs (Empty) returns (GetIACAsReply) {}
rpc Status (v1.status.StatusRequest) returns (v1.status.StatusReply) {}
}

SD-JWT Credential​

message MakeSDJWTRequest {
string scope = 1; // Credential scope (e.g., "diploma")
bytes documentData = 2; // JSON document data
jwk jwk = 3; // Holder's public key for binding
string integrity = 5; // Integrity token
bytes vctm = 6; // VCTM JSON bytes
}

mDL/mDoc Credential​

message MakeMDocRequest {
string scope = 1; // Credential scope
reserved 2; // was: string doc_type; the doctype now comes from mddl
bytes document_data = 3; // JSON encoded mDL data
bytes device_public_key = 4; // CBOR encoded COSE_Key
string device_key_format = 5; // "cose", "jwk", or "x509"
bytes mddl = 6; // Raw MDDL (mso_mdoc) schema JSON bytes
}

Connection​

The generated Go stubs live under internal/ in the vc repository and cannot be imported from other modules; generate your own client from proto/v1-issuer.proto. The gRPC server listens on issuer.grpc_server.addr (default :8090).

import (
"google.golang.org/grpc"
pb "example.org/yourmodule/gen/issuer/apiv1_issuer" // generated from proto/v1-issuer.proto
)

conn, err := grpc.Dial("issuer.example.org:8090", grpc.WithTransportCredentials(creds))
client := pb.NewIssuerServiceClient(conn)

reply, err := client.MakeSDJWT(ctx, &pb.MakeSDJWTRequest{
Scope: "diploma",
DocumentData: documentJSON,
Jwk: holderKey,
Vctm: vctmBytes,
})

Batch Issuance​

For large-scale credential provisioning, use the bulk upload and bulk identity mapping endpoints:

1. Bulk Create Identity Mappings​

curl -X POST https://issuer.example.org/api/v1/identity/mapping/bulk \
-H "Authorization: Bearer $JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"mappings": {
"emp1": { "authentic_source": "hr.example.org", "authentic_source_person_id": "EMP-001", "attributes": { "family_name": "Smith", "given_name": "Alice", "birth_date": "1990-05-15" } },
"emp2": { "authentic_source": "hr.example.org", "authentic_source_person_id": "EMP-002", "attributes": { "family_name": "Jones", "given_name": "Bob", "birth_date": "1985-03-22" } }
}
}'

2. Bulk Upload Documents​

curl -X POST https://issuer.example.org/api/v1/datastore/bulk \
-H "Authorization: Bearer $JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"documents": {
"doc1": { "meta": { "authentic_source": "hr.example.org", "scope": "diploma", "document_id": "diploma-001" }, "identity_mapping_ids": ["EMP-001"], "document_data": { "degree": "BSc" } },
"doc2": { "meta": { "authentic_source": "hr.example.org", "scope": "diploma", "document_id": "diploma-002" }, "identity_mapping_ids": ["EMP-002"], "document_data": { "degree": "MSc" } }
}
}'

3. Generate Offers​

Navigate users to the credential offer UI:

https://issuer.example.org/offers/diploma

Configuration​

API Authentication​

Configure authentication for the /api/v1 route group. Use jwks or oidc for token-based auth:

apigw:
api_server:
api_auth:
jwks:
enable: true
jwks_url: "https://auth.example.com/.well-known/jwks.json"
issuer: "https://auth.example.com"
audience: "vc-issuer"
# Optional: SPOCP rules for per-endpoint authorization
rules:
- "(vc (service apigw)(method POST)(path /api/v1/datastore/)(subject hr-system)(authentic_source SUNET)(scope diploma))"
- "(vc (service apigw)(method DELETE)(path /api/v1/datastore)(subject hr-admin)(authentic_source *)(scope *))"
# Or load rules from a file:
# rules_file: "/etc/vc/spocp-rules.txt"

JWT Bearer via OIDC Discovery​

apigw:
api_server:
api_auth:
oidc:
enable: true
issuer_url: "https://auth.example.com"
audience: "vc-issuer"
# client_id and redirect_uri are required whenever oidc.enable is true
client_id: "vc-apigw"
redirect_uri: "https://issuer.example.org/ui/callback"

When SPOCP rules are configured, each request is evaluated as a six-part s-expression (all six parts are mandatory and must appear in this exact order):

(vc (service apigw)(method <HTTP_METHOD>)(path <REQUEST_PATH>)(subject <JWT_SUBJECT>)(authentic_source <AS>)(scope <SCOPE>))

If no rules are configured, any valid JWT grants full access. The subject is resolved from the eppn or email claim in the JWT (not the standard sub claim).

Strict Enforcement

Since v0.6.2, the issuer strictly validates that all six s-expression parts are present and in the correct order. Rules with missing or reordered parts will be rejected at startup.

Pre-Authorized Offers​

A scope can only be minted as a pre-authorized offer if its datastore entry declares auth_provider: preauth. POST /api/v1/datastore/preauth_offer rejects anything else with invalid_scope, so the endpoint cannot be used to bypass the SAML/OIDC/OpenID4VP flow another scope declares.

apigw:
auth_providers:
preauth:
# Adds a numeric transaction code (PIN) to every pre-authorized offer,
# returned as tx_code and deliverable to the user out-of-band.
enable_pin: true

data_sources:
datastore:
scopes:
diploma:
auth_provider: preauth

The reply carries credential_offer, credential_offer_url, and tx_code when enable_pin is set.

Credential Metadata​

Map credential scopes to VCTM files:

common:
credential_metadata:
diploma:
vctm_file_path: "/metadata/vctm_diploma.json"
format: "dc+sd-jwt"
employee_badge:
vctm_file_path: "/metadata/vctm_employee.json"
format: "dc+sd-jwt"

Identity Matching​

Identity matching uses the attributes map stored in identity mappings. When resolving, all provided attributes must match exactly. Common attributes:

  • family_name
  • given_name
  • birth_date

The authentic source defines which attributes to store when creating identity mappings.


Error Handling​

Error Response Format​

Errors are returned as a single error object with a title and a free-form details payload:

{
"error": {
"title": "NO_DOCUMENT_FOUND",
"details": "no document found"
}
}

For validation failures, details carries the per-field breakdown rather than a string.

Common Error Titles​

TitleDescription
NO_DOCUMENT_FOUNDDocument does not exist
NO_DOCUMENT_DATAThe document carries no document_data
DOCUMENT_ALREADY_EXISTSA document with this key is already stored
DOCUMENT_IS_REVOKEDThe credential has been revoked
DOCUMENT_VALIDATION_FAILEDDocument data failed VCTM schema validation
NO_IDENTITY_FOUNDNo identity mapping matched the supplied attributes
DUPLICATE_KEYUnique-key conflict in storage
NO_TRANSACTION_IDRequired transaction_id missing
NO_REVOCATION_IDRequired revocation_id missing
ERR_NO_KNOWN_VCTThe requested credential type is not configured
INTERNAL_SERVER_ERRORUnhandled server-side failure

Security Considerations​

  1. JWT Token Security: Use short-lived tokens, validate issuer/audience claims, and rotate signing keys regularly
  2. SPOCP Authorization: Define fine-grained rules to restrict which subjects can access which endpoints
  3. mTLS for gRPC: Use mutual TLS for the internal Issuer gRPC service (configurable via grpc_server.tls)
  4. Input Validation: The issuer validates all document data against VCTM schemas
  5. Audit Logging: All API operations are logged for compliance (configurable webhook destinations)
  6. Rate Limiting: apigw has built-in per-IP rate limiting (apigw.rate_limit, including datastore_requests_per_minute); add reverse-proxy limits on top if needed

Next Steps​