Universal Inbox API
Lower-level inbox issuance API — use when you need control that send() doesn't expose.
The send() method is the recommended path for issuing credentials. However, for advanced use cases requiring full control over the inbox issuance process, you can use the sendCredentialViaInbox method directly.
You might drop down to this API when you need:
Custom delivery suppression (e.g., generating a claim link without sending an email)
Custom branding or configuration per issuance
Webhook-driven status tracking or an ongoing consent relationship
This approach assumes you are familiar with the core concepts of the Universal Inbox and have a valid API token & signing authority set up.
About the examples
The REST examples below use a tiny helper so each recipe stays short. It's plain fetch against your network's base URL with a scoped API token:
const API_BASE = 'https://network.learncard.com/api';
const learncardApiClient = {
get: async path => {
const res = await fetch(`${API_BASE}${path}`, {
headers: { Authorization: `Bearer ${process.env.LEARNCARD_API_TOKEN}` },
});
if (!res.ok) throw new Error(`${path} failed: ${res.status} ${await res.text()}`);
return res.json();
},
post: async (path, body) => {
const res = await fetch(`${API_BASE}${path}`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LEARNCARD_API_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`${path} failed: ${res.status} ${await res.text()}`);
return res.json();
},
};Issue a batch
POST /api/inbox/issue-batch requires inbox:write and returns HTTP 202 with { batchId, status: 'QUEUED', createdAt }. The SDK equivalent is learnCard.invoke.sendCredentialBatchViaInbox(batch) (also available as sendCredentialsViaInbox); tRPC uses inbox.issueBatch. An identical requestId retry returns the original batch ID and its current state, which may already be PROCESSING, COMPLETED, or NEEDS_RECONCILIATION. Receipt status is an advisory snapshot. Always poll the batch endpoint and use done to determine whether processing has finished, including unconfirmed outcomes.
GET /api/inbox/batches/{batchId} requires inbox:read and the submitting issuer profile. Use learnCard.invoke.getInboxCredentialBatch(batchId) or tRPC inbox.getBatch({ batchId }). Missing or inaccessible jobs return 404.
Submission items
1–100 items, each with a recipient and either credential or template URI.
configuration
Shared defaults deep-merged with item overrides; arrays replace defaults.
requestId
Optional 1–256 character submission ID. Identical retries return the same batch for 24 hours without charging again; changed input conflicts.
items[].idempotencyKey
Optional item key, up to 256 characters; successful replay window is 24 hours per issuer.
Polling done
True when no items remain queued or processing. Does not imply every issuance succeeded or was confirmed.
Polling status
QUEUED, PROCESSING, COMPLETED, or NEEDS_RECONCILIATION.
Polling items
Ordered entries with original index, processing state, and optional result.
Success result
success: true, index, optional idempotencyKey, issuanceId, credential status, recipient, optional claimUrl, recipientDid, guardianStatus, and deduplicated.
Failure result
success: false, index, recipient, optional idempotencyKey, error.code, error.message, optional error.reason; known issuance ID and claim URL may accompany uncertain outcomes.
summary
total, succeeded, failed, deduplicated, completed, pending, unconfirmed.
Set configuration.refresh: true to enable managed credential refresh for the batch. Each items[].configuration.refresh overrides that default; explicit false disables refresh for that item. The existing top-level items[].refresh is also supported: precedence is item configuration, then item top-level flag, then batch configuration. Refresh requires credentials:write in addition to inbox:write, managed refresh enabled on the server, unsigned content, and a registered signing authority. Successful refreshable items include result.refresh; retain this receipt to publish future versions of the same credential. Idempotent replays preserve the receipt.
Polling returns HTTP 200 even with item failures. Results remain available for 30 days after all items reach a terminal state, including NEEDS_RECONCILIATION. The original payload is removed when no items remain queued or processing. Unresolved client-keyed replay reservations remain blocked even after the job and results expire. Internal reservations for unkeyed items are collected after their batch items are pruned. NEEDS_RECONCILIATION can coexist with unfinished items; done stays false until they finish. completed counts terminal items regardless of outcome, including unconfirmed items. Counts are disjoint: succeeded + failed + unconfirmed + pending === total; deduplicated is a subset of succeeded.
Use the SDK polling helper for a bounded deadline, backoff, and cancellation:
The interval increases by 1.5× to a maximum of ten seconds (or the initial interval, if larger). Timeout rejects with TimeoutError; abort rejects with the signal's reason. Neither cancels the job. Keep the batch ID to resume polling. Scripts can use sendCredentialsViaInboxAndWait(input, { onSubmitted: receipt => saveReceipt(receipt) }) to submit once and wait. Both read and write scopes are required for this helper.
Identical keyed item retries replay the same issuance with deduplicated: true. Changed payloads conflict. Later occurrences of a key within a batch always conflict. Side-effect-free failures release the key; transient preparation and signing errors retry up to five worker attempts before delivery. An overlapping key also retries with backoff (five total attempts); if still blocked, the result retains IN_PROGRESS. Uncertain delivery outcomes retain the key. Never bypass an uncertain result by issuing with a new key.
Bodies over 4 MiB return 413; quota exhaustion returns 429. The default quota is 10,000 admitted items per issuer per hour. Rejected submissions and internal worker retries do not consume quota; admitted item replays and failures do.
Missing credentials/templates and guardian self-approval are rejected with HTTP 400 before admission or quota charging. REST validation issues contain message and path; for item issues, path[1] is the zero-based item index. Effective guardian defaults are checked after applying item overrides. Configuration defaults are additive: undefined inherits a default. An item may set guardianEmail: null to clear that batch-level default for the recipient.
Item error.code
error.reason
Action
CONFLICT
DUPLICATE_KEY
Remove later occurrences of the key within this batch.
CONFLICT
IDEMPOTENCY_MISMATCH
Restore the original input for this key; use a new key only for an intentionally new issuance.
CONFLICT
IN_PROGRESS
Another attempt owns the key or remains unconfirmed. Retry the same key later; reconcile if it stays blocked.
CONFLICT
UNCONFIRMED
Check the known issuance ID and reconcile; never bypass it with a new key.
BAD_REQUEST
—
Correct the input before resubmitting.
NOT_FOUND
—
Check the template or issuer still exists.
UNAUTHORIZED / FORBIDDEN
—
Check permissions and signing-authority access.
INTERNAL_SERVER_ERROR
—
Safe preparation or worker retries were exhausted; retry the same key.
For 5,000 recipients, create 50 batches of at most 100 items; split earlier when JSON approaches 4 MiB. Persist a stable requestId for each chunk and an idempotencyKey for each issuance before sending. Start with two in-flight batches, waiting for done before submitting the next chunk. At the default 10,000-item hourly quota, a 5,000-item run consumes half the window; resubmitting all items under new request IDs consumes the remaining half, even if every item replays. Retry a lost submission with its original request ID within 24 hours. On HTTP 429, stop admissions until the issuer's hourly window resets (at most one hour).
Issuance runs in a dedicated background queue; dispatch can take up to about a minute. For an immediate single-item result, use sendCredentialViaInbox. Single issuance remains synchronous. Both routes accept configuration.guardianEmail; it must differ from the recipient's email, ignoring case. See batch issuance for polling, worker timeouts, and recovery guidance.
The Simplest Case: Fire and Forget
Your goal is to send a single, verifiable record to a user. You want our system to handle all the complexity of signing the credential and notifying the user.
This is the most common use case, perfect for one-off issuances like a course completion certificate.
The Recipe: Make a POST request to the /inbox/issue endpoint with only two required fields: recipient and a signed or unsigned credential. An unsigned credential requires a configured signing authority.
Example:
Have you configured your default Primary Signing Authority?
If you get an error about a missing signing authority, ensure you've set one up following this guide. When you send an unsigned credential with Universal Inbox, it will use your primary signing authority to sign the credential when a user claims it.
If you'd like to use a custom signing authority, or specify it per request:
What Happens:
Our system receives the unsigned credential data.
It sends a professionally designed email to
student@example.comwith a secure link to claim their record.When the student claims their record, it automatically signs it using your default Primary Signing Authority attached to your profile.
You're done. The rest of the user onboarding and claim process is handled for you.
Customizing the User Experience
Your goal is to send a credential, but you want the notification email to be branded with your organization's identity to build trust and recognition.
The Recipe: Use the optional configuration.delivery.template.model object to provide your branding details.
Example:
What Happens: The email sent to the student will now feature the State University name and logo prominently, creating a more professional and trustworthy experience.
Taking Control of Delivery and Status
You have more advanced needs. You might want to deliver the claim link through your own system (e.g., inside your web portal) or need to know precisely when a user has successfully claimed their record.
Recipe 3a: Suppressing Delivery
Goal: You want to get a claimUrl from our API but prevent us from sending any emails or texts.
The Recipe: Set configuration.delivery.suppress to true.
Example:
Recipe 3b: Tracking Status with Webhooks
Goal: You need your system to be notified when a user successfully claims their credential so you can update your internal database.
The Recipe: Provide a configuration.webhookUrl.
Example:
What Happens: When the user claims their record, our system will send a POST request to your webhook URL with a payload containing the issuanceId, a status of CLAIMED, and the user's permanent recipientDid.
Want an ongoing relationship instead of one-off sends?
If you'll issue to the same people repeatedly, have them connect once through a ConsentFlow — after that, credentials land directly in their LearnCard with no email or claim link. See Connect a User's LearnCard to Your Platform.
Managed refresh
sendCredentialViaInbox / POST /api/inbox/issue accepts refresh: true and an optional idempotencyKey. API tokens need inbox:write plus credentials:write. Supply an unsigned, single-subject credential and a registered signing authority (explicit or primary).
The response includes refresh allocation metadata: refreshId, refreshService, credentialId, issuerDid, optional credentialStatus, and holderDid only once bound. Pending publication uses publishCredentialRefresh in signing-authority mode with the same authority; claim signs the newest pending revision and clears encrypted inbox escrow. After claim, getInboxCredential(issuanceId) returns the bound metadata receipt under refresh, allowing normal subsequent publication without reading the holder's claims.
See Refresh through Universal Inbox for runnable SDK request shapes, idempotency, encryption, and claim behavior.
Last updated
Was this helpful?