Issue a credential

One call turns an issuance profile plus claim data into a credential offer URI. Render it as a QR code or a link; the wallet does the rest.

POST/issuers/{issuerId}/create-credential-offer

Create an offer

Request

curl "$LUTRAID_API/v1/organizations/$ORG/issuers/$ISSUER/create-credential-offer" \
  -H "X-API-Key: $LUTRAID_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "issuanceProfileId": "8f1b…",
    "userPinRequired": true,
    "credentialOfferMode": "auto",
    "data": [
      { "documentTypeId": "pid-sd-jwt", "credentialSubject": { "given_name": "Ana", "family_name": "Novak" } }
    ]
  }'
{
  "credentialOfferId": "3c9e…",
  "historyId": "b71d…",
  "credentialOffer": "openid-credential-offer://?credential_offer_uri=https%3A%2F%2F…",
  "transactionCode": "418302",
  "deliveryMode": "direct",
  "recipientEmail": null,
  "deliveryStatus": "not_requested",
  "expiresAt": "2026-10-19T09:51:00.000Z"
}
  • Name
    issuanceProfileId
    Type
    uuid
    Description

    Required. The issuance profile assigned to this issuer; it decides formats, policies and grant type. The dashboard calls it a document: copy the Document ID from Setup → Documents.

  • Name
    data
    Type
    array
    Description

    Required. One entry per document type: documentTypeId plus its credentialSubject claims. This API does not define or validate claim names beyond what the document type was configured with.

  • Name
    userPinRequired
    Type
    boolean
    Description

    Returns a transactionCode, the PIN the wallet asks for. It is returned once, in this response, and never in an idempotent replay. LutraID never puts it in the offer email, so give it to the holder another way (on screen, in person or by phone), separately from the link. The wallet sends it as tx_code to the token endpoint.

  • Name
    deliveryMode
    Type
    string
    Description

    direct (default) returns the offer for you to render. email also sends it to recipientEmail.

  • Name
    recipientEmail
    Type
    string
    Description

    Required with deliveryMode: "email".

  • Name
    recipientLanguage
    Type
    string
    Description

    Language of the offer email: en (default), de or sl.

  • Name
    credentialConfigurationIds
    Type
    array
    Description

    Offer only these credential configurations of the issuance profile, for example to send again only the documents a wallet did not add. data is then needed only for their document types. Omit to offer all of them.

  • Name
    credentialOfferMode
    Type
    string
    Description

    auto (recommended), by_value or by_reference.

credentialOffer is an openid-credential-offer:// deep link. There is no QR endpoint — render the string yourself. The offer expires 10 minutes after creation; this isn't currently configurable.

Offer modes

ModeBehaviour
autoBy value while the encoded offer stays under ~1000 characters, by reference above it
by_valueAlways inline — fails if the offer is too large
by_referenceAlways a credential_offer_uri the wallet fetches

Use auto unless you have a wallet that cannot dereference credential_offer_uri.

Delivery

With deliveryMode: "direct" you render the URI yourself. With "email", LutraID also emails the link to recipientEmail in recipientLanguage, and deliveryStatus starts as pending. The email never contains the PIN: either give the holder the transactionCode another way or set userPinRequired to false for email offers.

Retrying safely

Send an Idempotency-Key header (8–200 characters, ^[A-Za-z0-9._~:-]+$) to retry a create without creating a second offer:

ReplayResult
Same key, same request bodyThe original offer as 200, without transactionCode
Same key, a different body, or the first call still running409
Same key after the offer's data is no longer kept409

Success on first creation is 201. Store the transactionCode from that response: a replay can't return it again.

POST/issuers/{issuerId}/create-bulk-credential-offer

Bulk offers

Creates up to 50 email offers in one call. Generate one batchId (a UUID) per bulk send and reuse it for every request and retry of that send. Each entry has its own id, unique within the batch.

cURL

curl "$LUTRAID_API/v1/organizations/$ORG/issuers/$ISSUER/create-bulk-credential-offer" \
  -H "X-API-Key: $LUTRAID_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "batchId": "0d6f2c1a-…",
    "issuanceProfileId": "8f1b…",
    "userPinRequired": false,
    "recipientLanguage": "de",
    "entries": [
      {
        "id": "row-1",
        "recipientEmail": "ana@example.com",
        "data": [{ "documentTypeId": "pid-sd-jwt", "credentialSubject": { "given_name": "Ana" } }]
      }
    ]
  }'

The response is 200 with one result per entry:

{
  "batchId": "0d6f2c1a-…",
  "results": [
    { "id": "row-1", "status": "created", "historyId": "…", "credentialOffer": "openid-credential-offer://…", "replayed": false },
    { "id": "row-2", "status": "invalid", "code": "VALIDATION_ERROR", "errors": [] },
    { "id": "row-3", "status": "failed", "code": "INTERNAL_ERROR" }
  ]
}
  • created: the offer exists. replayed: true means an earlier request of this batch created it; transactionCode is then absent.
  • invalid: the entry's data doesn't fit the document type. errors point into the entry. Fix and resend that row.
  • failed: the row could not be created. Resend it with the same batchId and id.

More than 50 entries returns 422; a stopped issuer returns 409 ISSUER_STOPPED. create-bulk-authorization-code-offer works the same way for the authorization code flow.

Errors: 400, 409 (idempotency conflict or ISSUER_STOPPED), 422 (validation), 429 (QUOTA_EXCEEDED, a plan limit) — see Errors.

Next

The holder's wallet now exchanges the pre-authorized code at the token endpoint and fetches the credential. Watch it with issuance status, or switch to the authorization code flow when claims must come from a login instead of your request body.

Was this page helpful?