WrapSplash.NET is a simple, async-first API wrapper for the popular Unsplash platform, built for .NET 8.0+. It provides full coverage of the Unsplash API with built-in retry policies, OAuth 2.0 and Bearer Token authentication, and a clean, ergonomic C# API surface.
Unsplash provides beautiful high quality free images and photos that you can download and use for any project without any attribution.
Before using the Unsplash API, you need to register as a developer and read the API Guidelines.
Note: Every application must abide by the API Guidelines. Specifically, remember to hotlink images and trigger a download when appropriate.
This library is a .NET port of the popular wrapsplash npm package.
- Features
- Installation
- Quick Start
- Configuration
- API Documentation
- Authorization
- Users APIs
- Photos APIs
- Search APIs
- Collections APIs
- List Collections
- List Featured Collections
- List Curated Collections
- Get a Collection
- Get a Curated Collection
- Get a Collection's Photos
- Get a Curated Collection's Photos
- List a Collection's Related Collections
- Create a New Collection
- Update an Existing Collection
- Delete a Collection
- Add a Photo to a Collection
- Remove a Photo from a Collection
- Stats APIs
- Error Handling
- Development
- Tests
- License
- Acknowledgements
- Full coverage of the Unsplash API endpoints
- Built-in retry policies with Polly
- OAuth 2.0 and Bearer Token authentication
- Configurable timeouts and retry behavior
- .NET 8.0+ with nullable reference types and XML documentation
- Async/await throughout
Install the package from NuGet:
dotnet add package WrapSplashusing WrapSplash.Models;
using WrapSplash.Services;
// Bearer Token authentication (simplest)
var client = new WrapSplashClient(new WrapSplashOptions
{
BearerToken = "your-bearer-token"
});
// Fetch a random photo
var photo = await client.GetARandomPhotoAsync();
// Search for photos
var results = await client.SearchAsync("nature", perPage: 10);
// Get a user's profile
var profile = await client.GetPublicProfileAsync("unsplash");
client.Dispose();var client = new WrapSplashClient(new WrapSplashOptions
{
AccessKey = "your-access-key",
SecretKey = "your-secret-key",
RedirectUri = "https://your-callback-url.com",
Code = "authorization-code"
});
// Generate a bearer token for write access
var token = await client.GenerateBearerTokenAsync();
client.Dispose();var client = new WrapSplashClient(new WrapSplashOptions
{
BearerToken = "your-token",
Timeout = 15000, // Request timeout in milliseconds (default: 10000)
Retries = 3, // Number of retry attempts (default: 2)
RetryDelayMs = 200 // Delay between retries in milliseconds (default: 100)
});| Option | Type | Default | Description |
|---|---|---|---|
AccessKey |
string |
null |
Unsplash API access key |
SecretKey |
string |
null |
Unsplash API secret key |
RedirectUri |
string |
null |
OAuth redirect URI |
Code |
string |
null |
OAuth authorization code |
BearerToken |
string |
null |
Bearer token for authenticated requests. When set, AccessKey/SecretKey/RedirectUri/Code are not required |
Timeout |
int |
10000 |
HTTP request timeout in milliseconds |
Retries |
int |
2 |
Number of retry attempts for failed requests |
RetryDelayMs |
int |
100 |
Delay between retry attempts in milliseconds |
The API base URL is https://api.unsplash.com/. Responses are sent as JSON.
When retrieving a list of objects, an abbreviated or summary version of that object is returned - i.e., a subset of its attributes. To get a full detailed version of that object, fetch it individually.
If an error occurs, whether on the server or client side, the error message(s) will be returned in an errors array:
422 Unprocessable Entity
{
"errors": ["Username is missing", "Password cannot be blank"]
}Many actions can be performed without requiring authentication from a specific user. To authenticate requests in this way, pass your application's access key via the HTTP Authorization header:
Authorization: Client-ID YOUR_ACCESS_KEY
You can also pass this value using a client_id query parameter:
https://api.unsplash.com/photos/?client_id=YOUR_ACCESS_KEY
If only your access key is sent, attempting to perform non-public actions that require user authorization will result in a 401 Unauthorized response.
The Unsplash API uses OAuth2 to authenticate and authorize Unsplash users. Unsplash's OAuth2 paths live at https://unsplash.com/oauth/.
Before using WrapSplash.NET:
- Developers are required to create a developer account from Unsplash.
- Create a new App from Your Apps page.
- Get the
AccessKey,SecretKey,RedirectUri, andCode. - If you have a Bearer Token, you can pass it directly. Otherwise, you can generate one using
GenerateBearerTokenAsync().
Note: The authorization code can be obtained by clicking the
Authorizelink next toCallback URLs. The authorization code is a one-time use code - you must generate it again if the action fails.
A method to generate a Bearer Token for write_access to private data. The constructor in this case requires AccessKey, SecretKey, RedirectUri, and Code to generate the bearer token.
Note: No additional parameters are required for this method.
var client = new WrapSplashClient(new WrapSplashOptions
{
AccessKey = "your-access-key",
SecretKey = "your-secret-key",
RedirectUri = "https://your-callback-url.com",
Code = "authorization-code"
});
var token = await client.GenerateBearerTokenAsync();If successful, the response will contain:
{
"access_token": "YOUR_BEARER_TOKEN_HERE",
"token_type": "bearer",
"scope": "public read_photos write_photos",
"created_at": 1436544465
}Then use the bearer token:
var client = new WrapSplashClient(new WrapSplashOptions
{
BearerToken = "YOUR_BEARER_TOKEN_HERE"
});Get the current user's profile. Requires the read_user scope and a Bearer Token.
GET /me
var profile = await client.GetCurrentUserProfileAsync();Update the current user's profile. Requires the write_user scope and a Bearer Token.
PUT /me
| Parameter | Type | Description | Optional |
|---|---|---|---|
username |
string |
The username of the current user | yes |
firstName |
string |
The first name of the current user | yes |
lastName |
string |
The last name of the current user | yes |
email |
string |
The email of the current user | yes |
url |
string |
The portfolio/personal URL of the current user | yes |
location |
string |
The location of the current user | yes |
bio |
string |
The bio of the current user | yes |
instagramUsername |
string |
The Instagram username of the current user | yes |
var result = await client.UpdateCurrentUserProfileAsync(
username: "new-username",
firstName: "John",
lastName: "Doe",
bio: "Photographer"
);Retrieve public details on a given user.
GET /users/:username
| Parameter | Type | Description | Optional |
|---|---|---|---|
username |
string |
The username of the particular user | no |
width |
int |
Width of the profile picture in pixels | yes |
height |
int |
Height of the profile picture in pixels | yes |
Note: When optional
width&heightare specified, the profile image will be included in theprofile_imageobject ascustom.
var profile = await client.GetPublicProfileAsync("unsplash", 600, 600);Retrieve a single user's portfolio link.
GET /users/:username/portfolio
| Parameter | Type | Description | Optional |
|---|---|---|---|
username |
string |
The username of the particular user | no |
var portfolio = await client.GetUserPortfolioAsync("unsplash");Get a list of photos uploaded by a particular user.
GET /users/:username/photos
| Parameter | Type | Description | Optional | Default |
|---|---|---|---|---|
username |
string |
The username of the particular user | no | |
page |
int |
Page number to retrieve | yes | 1 |
perPage |
int |
Number of items per page | yes | 10 |
stats |
bool |
Show the stats for each user's photo | yes | false |
resolution |
string |
The frequency of the stats | yes | days |
quantity |
int |
The amount for each stat | yes | 30 |
orderBy |
string |
How to sort the photos. Valid values: latest, oldest, popular |
yes | latest |
var photos = await client.GetUserPhotosAsync("unsplash", page: 1, perPage: 10, orderBy: "latest");Get a list of photos liked by a user.
GET /users/:username/likes
| Parameter | Type | Description | Optional | Default |
|---|---|---|---|---|
username |
string |
The username of the particular user | no | |
page |
int |
Page number to retrieve | yes | 1 |
perPage |
int |
Number of items per page | yes | 10 |
orderBy |
string |
How to sort the photos. Valid values: latest, oldest, popular |
yes | latest |
var liked = await client.GetUserLikedPhotosAsync("unsplash", page: 1, perPage: 10);Get a list of collections created by the user.
GET /users/:username/collections
| Parameter | Type | Description | Optional | Default |
|---|---|---|---|---|
username |
string |
The username of the particular user | no | |
page |
int |
Page number to retrieve | yes | 1 |
perPage |
int |
Number of items per page | yes | 10 |
var collections = await client.GetUserCollectionsAsync("unsplash", page: 1, perPage: 10);Get a user's account statistics.
GET /users/:username/statistics
| Parameter | Type | Description | Optional | Default |
|---|---|---|---|---|
username |
string |
The username of the particular user | no | |
resolution |
string |
The frequency of the stats | yes | days |
quantity |
int |
The amount for each stat | yes | 30 |
var stats = await client.GetUserStatisticsAsync("unsplash", "days", 30);Get a single page from the list of all photos.
GET /photos
| Parameter | Type | Description | Optional | Default |
|---|---|---|---|---|
page |
int |
Page number to retrieve | yes | 1 |
perPage |
int |
Number of items per page | yes | 10 |
orderBy |
string |
How to sort the photos. Valid values: latest, oldest, popular |
yes | latest |
var photos = await client.ListPhotosAsync(page: 1, perPage: 10, orderBy: "latest");Get a single page from the list of the curated photos.
GET /photos/curated
| Parameter | Type | Description | Optional | Default |
|---|---|---|---|---|
page |
int |
Page number to retrieve | yes | 1 |
perPage |
int |
Number of items per page | yes | 10 |
orderBy |
string |
How to sort the photos. Valid values: latest, oldest, popular |
yes | latest |
var photos = await client.ListCuratedPhotosAsync(page: 1, perPage: 10);Retrieve a single photo.
GET /photos/:id
| Parameter | Type | Description | Optional |
|---|---|---|---|
id |
string |
The photo's ID | no |
width |
int |
Image width in pixels | yes |
height |
int |
Image height in pixels | yes |
rect |
string |
4 comma-separated integers representing x, y, width, height of the cropped rectangle | yes |
Note: Supplying the optional
widthorheightparameters will result in the custom photo URL being added to theurlsobject.
var photo = await client.GetAPhotoAsync("photo-id", 500, 500);
// Or use the alias
var photo = await client.GetPhotoAsync("photo-id", 500, 500);Retrieve a single random photo, given optional filters.
GET /photos/random
| Parameter | Type | Description | Optional | Default |
|---|---|---|---|---|
collections |
string |
The public collection ID(s) to filter selection. If multiple, comma-separated | yes | |
featured |
bool |
Limit selection to featured photos | yes | false |
username |
string |
Limit selection to a single user | yes | |
query |
string |
Limit selection to photos matching a search term | yes | |
width |
int |
The image width in pixels | yes | |
height |
int |
The image height in pixels | yes | |
orientation |
string |
Filter by photo orientation. Valid values: landscape, portrait, squarish |
yes | landscape |
count |
int |
The number of photos to return (max: 30) | yes | 1 |
Note: You can't use the
collectionsandqueryparameters in the same request. When supplying acountparameter - and only then - the response will be an array of photos, even if the value ofcountis 1.
var photo = await client.GetARandomPhotoAsync();
// Or with filters
var photos = await client.GetARandomPhotoAsync(
query: "nature",
orientation: "landscape",
count: 5
);Retrieve total number of downloads, views and likes of a single photo, as well as the historical breakdown of these stats in a specific timeframe (default is 30 days).
GET /photos/:id/statistics
| Parameter | Type | Description | Optional | Default |
|---|---|---|---|---|
id |
string |
The photo's ID | no | |
resolution |
string |
The frequency of the stats | yes | days |
quantity |
int |
The amount for each stat (1-30) | yes | 30 |
Note: Currently, the only resolution param supported is
days. The quantity param can be any number between 1 and 30.
var stats = await client.GetPhotoStatisticsAsync("photo-id", "days", 10);Retrieve a single photo's download link. Preferably hit this endpoint if a photo is downloaded in your application for use (e.g., to be displayed on a blog article, to be shared on social media).
GET /photos/:id/download
| Parameter | Type | Description | Optional |
|---|---|---|---|
id |
string |
The photo's ID | no |
Note: This is different than the concept of a view, which is tracked automatically when you hotlink an image.
var link = await client.GetPhotoLinkAsync("photo-id");Update a photo on behalf of the logged-in user. This requires the write_photos scope and a Bearer Token.
PUT /photos/:id
| Parameter | Type | Description | Optional |
|---|---|---|---|
id |
string |
The photo's ID | no |
location |
Dictionary<string, object?> |
The location object holding location data | yes |
exif |
Dictionary<string, object?> |
The EXIF object holding EXIF data | yes |
| Key | Description |
|---|---|
latitude |
The photo location's latitude |
longitude |
The photo location's longitude |
name |
The photo location's name |
city |
The photo location's city |
country |
The photo location's country |
confidential |
The photo location's confidentiality |
| Key | Description |
|---|---|
make |
Camera's brand |
model |
Camera's model |
exposure_time |
Camera's exposure time |
aperture_value |
Camera's aperture value |
focal_length |
Camera's focal length |
iso_speed_ratings |
Camera's ISO |
var location = new Dictionary<string, object?> { { "country", "India" } };
var exif = new Dictionary<string, object?> { { "make", "Canon" }, { "model", "EOS R5" } };
var result = await client.UpdatePhotoAsync("photo-id", location, exif);Like a photo on behalf of the logged-in user. This requires the write_likes scope.
POST /photos/:id/like
| Parameter | Type | Description | Optional |
|---|---|---|---|
id |
string |
The photo's ID | no |
Note: This action is idempotent; sending the POST request to a single photo multiple times has no additional effect.
var result = await client.LikePhotoAsync("photo-id");Remove a user's like of a photo.
DELETE /photos/:id/like
| Parameter | Type | Description | Optional |
|---|---|---|---|
id |
string |
The photo's ID | no |
Note: This action is idempotent; sending the DELETE request to a single photo multiple times has no additional effect.
var result = await client.UnlikePhotoAsync("photo-id");Get a single page of photo results for a particular query.
GET /search/photos
| Parameter | Type | Description | Optional | Default |
|---|---|---|---|---|
query |
string |
The search query | no | |
page |
int |
Page number to retrieve | yes | 1 |
perPage |
int |
Number of items per page | yes | 10 |
collections |
string |
Collection ID(s) to narrow search. If multiple, comma-separated | yes | |
orientation |
string |
Filter search results by photo orientation. Valid values: landscape, portrait, squarish |
yes |
var results = await client.SearchAsync("cars", page: 1, perPage: 10, orientation: "landscape");Get a single page of collection results for a query.
GET /search/collections
| Parameter | Type | Description | Optional | Default |
|---|---|---|---|---|
query |
string |
The search query | no | |
page |
int |
Page number to retrieve | yes | 1 |
perPage |
int |
Number of items per page | yes | 10 |
var results = await client.SearchCollectionsAsync("cars", page: 1, perPage: 10);Get a single page of user results for a query.
GET /search/users
| Parameter | Type | Description | Optional | Default |
|---|---|---|---|---|
query |
string |
The search query | no | |
page |
int |
Page number to retrieve | yes | 1 |
perPage |
int |
Number of items per page | yes | 10 |
var results = await client.SearchUsersAsync("sandeep", page: 1, perPage: 10);Collections have the following link relations:
| Rel | Description |
|---|---|
self |
API location of this collection |
html |
HTML location of this collection |
photos |
API location of this collection's photos |
related |
API location of this collection's related collections (non-curated collections only) |
download |
Download location of this collection's zip file (curated collections only) |
Get a single page from the list of all collections.
GET /collections
| Parameter | Type | Description | Optional | Default |
|---|---|---|---|---|
page |
int |
Page number to retrieve | yes | 1 |
perPage |
int |
Number of items per page | yes | 10 |
var collections = await client.ListCollectionsAsync(page: 1, perPage: 10);Get a single page from the list of featured collections.
GET /collections/featured
| Parameter | Type | Description | Optional | Default |
|---|---|---|---|---|
page |
int |
Page number to retrieve | yes | 1 |
perPage |
int |
Number of items per page | yes | 10 |
var collections = await client.ListFeaturedCollectionsAsync(page: 1, perPage: 10);Get a single page from the list of curated collections.
GET /collections/curated
| Parameter | Type | Description | Optional | Default |
|---|---|---|---|---|
page |
int |
Page number to retrieve | yes | 1 |
perPage |
int |
Number of items per page | yes | 10 |
var collections = await client.ListCuratedCollectionsAsync(page: 1, perPage: 10);Retrieve a single collection. To view a user's private collections, the read_collections scope is required.
GET /collections/:id
| Parameter | Type | Description | Optional |
|---|---|---|---|
id |
string |
The collection ID | no |
var collection = await client.GetCollectionAsync("collection-id");Retrieve a single curated collection. To view a user's private collections, the read_collections scope is required.
GET /collections/curated/:id
| Parameter | Type | Description | Optional |
|---|---|---|---|
id |
string |
The collection ID | no |
var collection = await client.GetCuratedCollectionAsync("collection-id");Retrieve a collection's photos.
GET /collections/:id/photos
| Parameter | Type | Description | Optional | Default |
|---|---|---|---|---|
id |
string |
The collection ID | no | |
page |
int |
Page number to retrieve | yes | 1 |
perPage |
int |
Number of items per page | yes | 10 |
var photos = await client.GetCollectionPhotosAsync("collection-id", page: 1, perPage: 10);Retrieve a curated collection's photos.
GET /collections/curated/:id/photos
| Parameter | Type | Description | Optional | Default |
|---|---|---|---|---|
id |
string |
The collection ID | no | |
page |
int |
Page number to retrieve | yes | 1 |
perPage |
int |
Number of items per page | yes | 10 |
var photos = await client.GetCuratedCollectionPhotosAsync("collection-id", page: 1, perPage: 10);Retrieve a list of collections related to this one.
GET /collections/:id/related
| Parameter | Type | Description | Optional |
|---|---|---|---|
id |
string |
The collection ID | no |
var related = await client.ListRelatedCollectionsAsync("collection-id");Create a new collection. This requires the write_collections scope.
POST /collections
| Parameter | Type | Description | Optional | Default |
|---|---|---|---|---|
title |
string |
The title of the collection | no | |
description |
string |
The collection's description | yes | |
isPrivate |
bool |
Whether to make this collection private | yes | false |
var collection = await client.CreateNewCollectionAsync("My Collection", "A collection of nature photos");
// Or use the alias
var collection = await client.CreateCollectionAsync("My Collection");Update an existing collection belonging to the logged-in user. This requires the write_collections scope.
PUT /collections/:id
| Parameter | Type | Description | Optional | Default |
|---|---|---|---|---|
id |
string |
The collection ID | no | |
title |
string |
The title of the collection | no | |
description |
string |
The collection's description | yes | |
isPrivate |
bool |
Whether to make this collection private | yes | false |
var result = await client.UpdateExistingCollectionAsync("collection-id", "Updated Title", "New description");
// Or use the alias
var result = await client.UpdateCollectionAsync("collection-id", "Updated Title");Delete a collection belonging to the logged-in user. This requires the write_collections scope.
DELETE /collections/:id
| Parameter | Type | Description | Optional |
|---|---|---|---|
id |
string |
The collection ID | no |
await client.DeleteCollectionAsync("collection-id");Add a photo to one of the logged-in user's collections. Requires the write_collections scope.
POST /collections/:collection_id/add
| Parameter | Type | Description | Optional |
|---|---|---|---|
collectionId |
string |
The collection ID | no |
photoId |
string |
The photo ID | no |
Note: If the photo is already in the collection, this action has no effect.
await client.AddPhotoToCollectionAsync("collection-id", "photo-id");Remove a photo from one of the logged-in user's collections. Requires the write_collections scope.
DELETE /collections/:collection_id/remove
| Parameter | Type | Description | Optional |
|---|---|---|---|
collectionId |
string |
The collection ID | no |
photoId |
string |
The photo ID | no |
await client.RemovePhotoFromCollectionAsync("collection-id", "photo-id");Get a list of counts for all of Unsplash.
GET /stats/total
var totals = await client.GetStatsTotalsAsync();Get the overall Unsplash stats for the past 30 days.
GET /stats/month
var monthStats = await client.GetStatsMonthAsync();The client throws WrapSplashException on API errors with HTTP status code information.
| Property | Type | Description |
|---|---|---|
StatusCode |
int? |
The HTTP status code of the failed request |
StatusText |
string? |
The HTTP status text of the failed request |
Cause |
object? |
The original exception that caused this error |
Message |
string |
The error message |
using WrapSplash.Models;
try
{
var photo = await client.GetAPhotoAsync("invalid-id");
}
catch (WrapSplashException ex)
{
Console.WriteLine($"Error {ex.StatusCode}: {ex.Message}");
}- 204 No Content: Returns
{ status: 204, statusText: "No Content", message: "Content Deleted" } - 403 Forbidden: Returns
{ status: 403, statusText: "Forbidden", message: "Rate Limit Exceeded" } - Other errors: Throws
WrapSplashExceptionwith the HTTP status code and error details
# Clone the repository
git clone https://github.com/Sandeepv68/wrapsplash.net.git
cd wrapsplash.net
# Restore dependencies
dotnet restore WrapSplash.slnx
# Build
dotnet build WrapSplash.slnx --configuration Release
# Run tests
dotnet test WrapSplash.slnx --configuration Release --verbosity normalWrapSplash.NET uses xUnit as the testing framework with Moq for mocking. Test files are available in the WrapSplash.NET.Tests/ folder.
dotnet test WrapSplash.NET.Tests --verbosity normalThis project uses GitHub Actions for continuous integration. On every push or pull request to the master branch, the CI pipeline:
- Restores dependencies
- Builds the solution
- Runs all tests
On release creation, the CI pipeline additionally:
- Packs the NuGet package
- Publishes to NuGet.org via OIDC trusted publishers
Copyright (c) 2026 - Sandeep Vattapparambil
Thanks, and Kudos to team Unsplash for creating a wonderful platform for sharing beautiful high quality free images and photos.
