Skip to content
33 changes: 33 additions & 0 deletions .changeset/clever-fans-roll.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
'@learncard/network-brain-service': minor
'@learncard/network-plugin': minor
'@learncard/types': minor
---

Add generic `send` method for ergonomic credential sending

**Features:**

- New `send` method that auto-issues credentials from boost templates
- Supports client-side credential issuance when available, falls back to signing authority
- Contract-aware: automatically routes through consent flow when recipient has consented
- Creates `RELATED_TO` relationship between newly created boosts and contracts

**Usage:**

```typescript
// Send using existing boost template
await lc.invoke.send({
type: 'boost',
recipient: 'userProfileId',
templateUri: 'urn:lc:boost:...',
});

// Send by creating a new boost
await lc.invoke.send({
type: 'boost',
recipient: 'userProfileId',
template: { credential: unsignedVC },
contractUri: 'urn:lc:contract:...', // optional
});
```
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,36 @@

Understand how, after consent is given, new credentials or data can be provided to a user or recorded about them by authorized parties (like contract owners or automated systems). This covers the rules and methods for data delivery based on established consent.

## Writing Credentials to Contracts <a href="#writing-credentials-to-contracts" id="writing-credentials-to-contracts"></a>
## The `send` Method with Contract Integration (Recommended)

Contract owners can write credentials to profiles that have consented to their contracts using:
The simplest way to write credentials while respecting consent flows is using the `send` method with a `contractUri`. This method automatically routes through consent terms when the recipient has consented.

```typescript
const result = await learnCard.invoke.send({
type: 'boost',
recipient: 'recipient-profile-id',
templateUri: 'urn:lc:boost:abc123',
contractUri: 'urn:lc:contract:xyz789', // Optional: routes via consent if applicable
});
```

When you provide a `contractUri`:

1. **Automatic routing**: If the recipient has consented to the contract, the credential routes through the consent flow
2. **Boost-contract relationship**: When creating a new boost on-the-fly, a `RELATED_TO` relationship is established between the boost and the contract
3. **Permission verification**: The system verifies you have permission to write in the credential's category

{% hint style="success" %}
**Fallback behavior**: If the recipient hasn't consented to the contract, the credential is still sent normallyβ€”the contract integration is additive, not blocking.
{% endhint %}

For more details on the `send` method, see the [Send Credentials How-To Guide](../../how-to-guides/send-credentials.md).

---

## Writing Credentials to Contracts (Direct) <a href="#writing-credentials-to-contracts" id="writing-credentials-to-contracts"></a>

For more granular control, contract owners can write credentials directly to profiles that have consented using:

* `writeCredentialToContract`: Direct credential writing
* `writeCredentialToContractViaSigningAuthority`: Using a signing authority
Expand Down
124 changes: 121 additions & 3 deletions docs/how-to-guides/send-credentials.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,130 @@
---
description: 'How-To Guide: Sending Credentials with Universal Inbox'
description: 'How-To Guide: Sending Credentials with LearnCard'
---

# Send Credentials

This guide provides practical, step-by-step recipes for using the Universal Inbox API. Its goal is to help you, the developer, get your job done quickly and elegantly. We'll start with the simplest possible use case and progressively add more powerful configurations.
This guide provides practical, step-by-step recipes for sending credentials. We'll start with the simplest possible use case and progressively add more powerful configurations.

This guide assumes you are familiar with the core concepts of the [Universal Inbox](../core-concepts/network-and-interactions/universal-inbox.md) and have [a valid API token](../sdks/learncard-network/authentication.md#id-2.-using-a-scoped-api-token) & [signing authority ](create-signing-authority.md)set up.
---

## Quick Start: The `send` Method (Recommended)

The `send` method is the simplest and most ergonomic way to send credentials to recipients. It handles credential issuance, signing, and delivery in a single call.

### Prerequisites

* LearnCard SDK initialized with `network: true`
* A [signing authority](create-signing-authority.md) configured (for server-side signing) **OR** local key material available (for client-side signing)

### Basic Usage

{% tabs %}
{% tab title="Using an Existing Boost Template" %}
```typescript
// Send a credential using an existing boost template
const result = await learnCard.invoke.send({
type: 'boost',
recipient: 'recipient-profile-id', // or DID
templateUri: 'urn:lc:boost:abc123',
});

console.log(result.credentialUri); // URI of the sent credential
console.log(result.uri); // URI of the boost template used
```
{% endtab %}

{% tab title="Creating a New Boost On-the-Fly" %}
```typescript
// Send by creating a new boost from an unsigned credential
const result = await learnCard.invoke.send({
type: 'boost',
recipient: 'recipient-profile-id',
template: {
credential: {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.2.json"
],
"type": ["VerifiableCredential", "OpenBadgeCredential"],
"name": "Course Completion",
"credentialSubject": {
"type": ["AchievementSubject"],
"achievement": {
"type": ["Achievement"],
"name": "Web Development 101",
"description": "Completed the Web Development fundamentals course.",
"criteria": {
"narrative": "Successfully completed all modules and passed the final assessment."
}
}
}
},
name: 'Web Development 101 Certificate',
category: 'Achievement',
},
});
```
{% endtab %}

{% tab title="With ConsentFlow Contract" %}
```typescript
// Send through a consent flow contract
// Automatically routes via consent terms if the recipient has consented
const result = await learnCard.invoke.send({
type: 'boost',
recipient: 'recipient-profile-id',
templateUri: 'urn:lc:boost:abc123',
contractUri: 'urn:lc:contract:xyz789', // Optional: link to consent contract
});
```
{% endtab %}
{% endtabs %}

### How It Works

1. **Resolves the recipient** - Looks up the profile by ID or uses a DID directly
2. **Prepares the credential** - Uses your template or creates a new boost on-the-fly
3. **Signs the credential** - Uses client-side signing if available, otherwise falls back to your registered signing authority
4. **Delivers the credential** - Sends via the LearnCard Network with optional consent flow routing

### Response

```typescript
interface SendResponse {
type: 'boost';
credentialUri: string; // URI of the issued credential
uri: string; // URI of the boost template
}
```

{% hint style="info" %}
**Contract Integration**: When you provide a `contractUri`, the method automatically:
- Checks if the recipient has consented to the contract
- Routes the credential through the consent flow if terms exist
- Creates a `RELATED_TO` relationship between new boosts and the contract
{% endhint %}

---

## Alternative: Universal Inbox API

Use the Universal Inbox API when you need to send credentials to **users who don't have a LearnCard profile yet**. This allows you to reach recipients via email or phone number, and they'll be guided through creating an account when they claim their credential.

This is ideal for:

- **Onboarding new users** - Send to an email or phone number, not a profile ID
- **Custom email templates and branding** - White-label the claim experience
- **Webhook notifications** - Get notified when credentials are claimed
- **Suppressing automatic delivery** - Handle notification yourself and just get a claim URL

{% hint style="info" %}
**When to use which:**
- **`send` method** β†’ Recipient already has a LearnCard profile (you have their profile ID or DID)
- **Universal Inbox API** β†’ Recipient may not have LearnCard yet (you have their email or phone)
{% endhint %}

This approach assumes you are familiar with the core concepts of the [Universal Inbox](../core-concepts/network-and-interactions/universal-inbox.md) and have [a valid API token](../sdks/learncard-network/authentication.md#id-2.-using-a-scoped-api-token) & [signing authority](create-signing-authority.md) set up.

## 1. The Simplest Case: Fire and Forget

Expand Down
49 changes: 49 additions & 0 deletions docs/sdks/learncard-network/usage-examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,53 @@ All requests require:

***

## πŸ“€ Sending Credentials

### The `send` Method (Recommended)

The simplest way to send credentials is using the `send` method, which handles credential issuance, signing, and delivery in a single call.

#### Using an Existing Boost Template

```typescript
const result = await learnCard.invoke.send({
type: 'boost',
recipient: 'recipient-profile-id', // Profile ID or DID
templateUri: 'urn:lc:boost:abc123',
});

// Returns: { type: 'boost', credentialUri: '...', uri: '...' }
```

#### Creating and Sending a New Boost

```typescript
const result = await learnCard.invoke.send({
type: 'boost',
recipient: 'recipient-profile-id',
template: {
credential: unsignedVC,
name: 'Course Completion Certificate',
category: 'Achievement',
},
});
```

#### With ConsentFlow Contract

```typescript
const result = await learnCard.invoke.send({
type: 'boost',
recipient: 'recipient-profile-id',
templateUri: 'urn:lc:boost:abc123',
contractUri: 'urn:lc:contract:xyz789', // Routes via consent terms if applicable
});
```

{% hint style="info" %}
**Signing Behavior**: The `send` method uses client-side signing when key material is available. Otherwise, it falls back to your registered signing authority.
{% endhint %}

***

##
47 changes: 46 additions & 1 deletion docs/tutorials/create-a-boost.md
Original file line number Diff line number Diff line change
Expand Up @@ -378,15 +378,60 @@ main().catch(err => console.error("Boost tutorial encountered an error:", err));

After the script runs, check the LearnCard apps associated with the recipient Profile IDs you used. Each should have received the "Tech Innovators Meetup - May 2025 Attendee" credential in your notifications.

---

## Bonus: Simplified Sending with the `send` Method

If you want to create and send a boost in a single call, you can use the new `send` method. This is ideal for quick issuance scenarios where you don't need to manage the boost template separately.

```typescript
// Create and send a boost in one call
const result = await learnCard.invoke.send({
type: 'boost',
recipient: 'recipient-profile-id',
template: {
credential: meetupAttendeeCredentialTemplate,
name: 'Tech Innovators Meetup - May 2025 Attendee',
category: 'Social Badge',
},
});

console.log('Credential sent! URI:', result.credentialUri);
console.log('Boost template URI:', result.uri);
```

Or, if you already have a boost template created:

```typescript
// Send using an existing boost template
const result = await learnCard.invoke.send({
type: 'boost',
recipient: 'recipient-profile-id',
templateUri: meetupBoostUri, // The URI from createBoost
});
```

{% hint style="success" %}
The `send` method automatically handles:
- **Signing**: Uses client-side signing if available, falls back to your signing authority
- **Issuance date**: Sets the current timestamp automatically
- **Recipient DID**: Populates the `credentialSubject.id` with the recipient's DID
{% endhint %}

For more details, see the [Send Credentials How-To Guide](../how-to-guides/send-credentials.md).

---

## Summary & What's Next

Fantastic! You've now learned how to: βœ… Understand the value of Boosts for reusable credential templates. βœ… Define the content for a Boost. βœ… Create a Boost using the LearnCard SDK. βœ… Send instances of that Boost to multiple recipients.
Fantastic! You've now learned how to: βœ… Understand the value of Boosts for reusable credential templates. βœ… Define the content for a Boost. βœ… Create a Boost using the LearnCard SDK. βœ… Send instances of that Boost to multiple recipients. βœ… Use the simplified `send` method for quick issuance.

Boosts are a powerful way to manage credentialing at scale. From here, you can explore:

* **Retrieving Boost Recipients:** Use `learnCard.invoke.getPaginatedBoostRecipients(boostUri)` to see who has been issued a credential from this Boost.
* **Boost Permissions:** Control who can edit, issue, or manage your Boosts. (See Boost Permission Model).
* **Boost Hierarchies:** Organize Boosts into parent-child relationships. (See Boost Hierarchies).
* Customizing **Display Options** for your Boosts to make them visually appealing in wallets.
* **ConsentFlow Integration:** Link boosts to consent contracts for automatic routing. (See [ConsentFlow Overview](../core-concepts/consent-and-permissions/consentflow-overview.md)).

Explore the [Boost Credentials Core Concept page](../core-concepts/credentials-and-data/boost-credentials.md) for more in-depth information on all the capabilities of Boosts!
41 changes: 41 additions & 0 deletions packages/learn-card-types/src/lcn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,47 @@ export const AutoBoostConfigValidator = z.object({
});
export type AutoBoostConfig = z.infer<typeof AutoBoostConfigValidator>;

const SendBoostTemplateValidator = BoostValidator.partial()
.omit({ uri: true, claimPermissions: true })
.extend({
credential: VCValidator.or(UnsignedVCValidator),
claimPermissions: BoostPermissionsValidator.partial().optional(),
skills: z
.array(z.object({ frameworkId: z.string(), id: z.string() }))
.min(1)
.optional(),
});

// Route-level validator (TRPC requires ZodObject, not discriminatedUnion)
export const SendBoostInputValidator = z
.object({
type: z.literal('boost'),
recipient: z.string(),
contractUri: z.string().optional(),
templateUri: z.string().optional(),
template: SendBoostTemplateValidator.optional(),
signedCredential: VCValidator.optional(),
})
.refine(data => data.templateUri || data.template, {
message: 'Either templateUri or template creation data must be provided.',
path: ['templateUri'],
});
export type SendBoostInput = z.infer<typeof SendBoostInputValidator>;

export const SendBoostResponseValidator = z.object({
type: z.literal('boost'),
credentialUri: z.string(),
uri: z.string(),
});
export type SendBoostResponse = z.infer<typeof SendBoostResponseValidator>;

// Plugin-level discriminated union (for extensibility)
export const SendInputValidator = z.discriminatedUnion('type', [SendBoostInputValidator]);
export type SendInput = z.infer<typeof SendInputValidator>;

export const SendResponseValidator = z.discriminatedUnion('type', [SendBoostResponseValidator]);
export type SendResponse = z.infer<typeof SendResponseValidator>;

export const ConsentFlowTermsStatusValidator = z.enum(['live', 'stale', 'withdrawn']);
export type ConsentFlowTermsStatus = z.infer<typeof ConsentFlowTermsStatusValidator>;

Expand Down
Loading
Loading