view release on metacpan or search on metacpan
# Apertur::SDK
Official Perl SDK for the [Apertur](https://apertur.ca) API. Supports API key and OAuth token authentication, session management, image uploads (plain and encrypted), long polling, webhook verification, and full resource CRUD.
## Installation
Requires Perl 5.26+ and is installed via standard CPAN tooling.
```bash
cpanm Apertur::SDK
```
Or from source:
```bash
perl Makefile.PL
make
make test
make install
```
## Quick Start
Create a client, open an upload session, and upload an image in a few lines. See the [API documentation](https://docs.apertur.ca) for a full overview.
```perl
use Apertur::SDK;
my $client = Apertur::SDK->new(api_key => 'aptr_live_...');
my $session = $client->sessions->create(label => 'My shoot');
my $image = $client->upload->image($session->{uuid}, '/path/to/photo.jpg');
print "Uploaded: $image->{id}\n";
```
## Authentication
The client accepts either a long-lived API key or a short-lived OAuth bearer token. Only one is required. See [Authentication documentation](https://docs.apertur.ca/authentication).
```perl
use Apertur::SDK;
my $client = Apertur::SDK->new(
api_key => 'aptr_live_...',
base_url => 'https://sandbox.api.aptr.ca',
);
```
Keys prefixed with `aptr_test_` automatically target the sandbox environment.
## Sessions
Upload sessions scope every image upload. You can create a session with optional settings, retrieve it, protect it with a password, and check delivery status. See [Sessions documentation](https://docs.apertur.ca/upload-sessions).
```perl
use Apertur::SDK;
my $client = Apertur::SDK->new(api_key => 'aptr_live_...');
# Create a session
my $session = $client->sessions->create(
label => 'Wedding reception',
password => 's3cr3t',
maxImages => 200,
);
# Retrieve session details
my $details = $client->sessions->get($session->{uuid});
# Verify a password-protected session before uploading
my $result = $client->sessions->verify_password($session->{uuid}, 's3cr3t');
# Check per-destination delivery status. Returns:
# { status => 'pending|active|completed|expired',
# files => [...], lastChanged => '<ISO 8601>' }
my $status = $client->sessions->delivery_status($session->{uuid});
# Long-poll: server holds the response up to 5 min until something changes.
# Passing poll_from automatically widens the per-request timeout to 360 s.
$status = $client->sessions->delivery_status(
$session->{uuid},
poll_from => $status->{lastChanged},
);
```
## Uploading Images
Upload a plain image using a file path or a scalar reference with raw bytes. For end-to-end encrypted uploads, use `image_encrypted` with the server's RSA public key. See [Upload documentation](https://docs.apertur.ca/upload-sessions).
```perl
use Apertur::SDK;
my $client = Apertur::SDK->new(api_key => 'aptr_live_...');
my $uuid = 'session-uuid-here';
# Upload from a file path
my $image = $client->upload->image($uuid, '/tmp/photo.jpg',
filename => 'photo.jpg',
mimeType => 'image/jpeg',
source => 'my-app',
);
# Upload from raw bytes
my $bytes = read_file_bytes('/tmp/photo.jpg');
my $image = $client->upload->image($uuid, \$bytes);
# Upload to a password-protected session
my $image = $client->upload->image($uuid, '/tmp/photo.jpg',
password => 's3cr3t',
);
# Encrypted upload
my $server_key = $client->encryption->get_server_key();
my $image = $client->upload->image_encrypted(
$uuid, '/tmp/photo.jpg', $server_key->{publicKey},
filename => 'photo.jpg',
mimeType => 'image/jpeg',
);
```
## Long Polling
Poll a session for new images, download each one, and acknowledge receipt to advance the queue. The `poll_and_process` helper loops automatically and calls your handler for every image. See [Long Polling documentation](https://docs.apertur.ca/long-po...
# Event webhook (HMAC method)
my $valid = verify_event_signature($body, $timestamp, $signature, $secret);
# Event webhook (Svix method)
my $valid = verify_svix_signature($body, $svix_id, $timestamp, $signature, $secret);
```
## Destinations
Destinations define where uploaded images are delivered. See [Destinations documentation](https://docs.apertur.ca/destinations).
```perl
use Apertur::SDK;
my $client = Apertur::SDK->new(api_key => 'aptr_live_...');
my $project_id = 'proj_...';
my $list = $client->destinations->list($project_id);
my $dest = $client->destinations->create($project_id,
```perl
use Apertur::SDK;
my $client = Apertur::SDK->new(api_key => 'aptr_live_...');
my $project_id = 'proj_...';
my $webhooks = $client->webhooks->list($project_id);
my $webhook = $client->webhooks->create($project_id,
url => 'https://example.com/webhooks/apertur',
events => ['image.uploaded', 'session.completed'],
);
$client->webhooks->update($project_id, $webhook->{id},
events => ['image.uploaded'],
);
$client->webhooks->test($project_id, $webhook->{id});
my $deliveries = $client->webhooks->deliveries($project_id, $webhook->{id},
page => 1,
limit => 25,
);
$client->webhooks->retry_delivery($project_id, $webhook->{id},
$deliveries->{data}[0]{id},
);
$client->webhooks->delete($project_id, $webhook->{id});
```
## Encryption
Apertur supports end-to-end encrypted uploads using RSA-OAEP + AES-256-GCM. Requires optional dependencies `Crypt::OpenSSL::RSA` and `CryptX`. See [Encryption documentation](https://docs.apertur.ca/encryption).
```perl
use Apertur::SDK;
my $client = Apertur::SDK->new(api_key => 'aptr_live_...');
my $server_key = $client->encryption->get_server_key();
my $image = $client->upload->image_encrypted(
'session-uuid-here',
'/tmp/photo.jpg',
$server_key->{publicKey},
filename => 'photo.jpg',
mimeType => 'image/jpeg',
);
print "Uploaded: $image->{id}\n";
```
use Apertur::SDK::Error;
use Apertur::SDK::Error::Authentication;
use Apertur::SDK::Error::NotFound;
use Apertur::SDK::Error::RateLimit;
use Apertur::SDK::Error::Validation;
my $client = Apertur::SDK->new(api_key => 'aptr_live_...');
eval {
my $session = $client->sessions->create(label => 'My shoot');
my $image = $client->upload->image($session->{uuid}, '/tmp/photo.jpg');
};
if (my $err = $@) {
if (ref $err && $err->isa('Apertur::SDK::Error::Authentication')) {
warn "Auth failed: " . $err->message . "\n";
}
elsif (ref $err && $err->isa('Apertur::SDK::Error::NotFound')) {
warn "Not found: " . $err->message . "\n";
}
elsif (ref $err && $err->isa('Apertur::SDK::Error::RateLimit')) {
my $retry = $err->retry_after // '?';
lib/Apertur/SDK.pm view on Meta::CPAN
my $http = Apertur::SDK::HTTPClient->new(
base_url => $base_url,
api_key => $args{api_key},
oauth_token => $args{oauth_token},
);
return bless {
env => $env,
_http => $http,
sessions => Apertur::SDK::Resource::Sessions->new(http => $http),
upload => Apertur::SDK::Resource::Upload->new(http => $http),
uploads => Apertur::SDK::Resource::Uploads->new(http => $http),
polling => Apertur::SDK::Resource::Polling->new(http => $http),
destinations => Apertur::SDK::Resource::Destinations->new(http => $http),
keys => Apertur::SDK::Resource::Keys->new(http => $http),
webhooks => Apertur::SDK::Resource::Webhooks->new(http => $http),
encryption => Apertur::SDK::Resource::Encryption->new(http => $http),
stats => Apertur::SDK::Resource::Stats->new(http => $http),
}, $class;
}
sub env { return $_[0]->{env} }
sub sessions { return $_[0]->{sessions} }
sub upload { return $_[0]->{upload} }
sub uploads { return $_[0]->{uploads} }
sub polling { return $_[0]->{polling} }
sub destinations { return $_[0]->{destinations} }
sub keys { return $_[0]->{keys} }
sub webhooks { return $_[0]->{webhooks} }
sub encryption { return $_[0]->{encryption} }
sub stats { return $_[0]->{stats} }
1;
__END__
lib/Apertur/SDK.pm view on Meta::CPAN
=head1 VERSION
Version 0.01
=head1 SYNOPSIS
use Apertur::SDK;
my $client = Apertur::SDK->new(api_key => 'aptr_live_...');
# Create an upload session
my $session = $client->sessions->create(label => 'My shoot');
# Upload an image
my $image = $client->upload->image($session->{uuid}, '/path/to/photo.jpg');
print "Uploaded: $image->{id}\n";
# Long polling
$client->polling->poll_and_process(
$session->{uuid},
sub {
my ($image, $data) = @_;
open my $fh, '>:raw', "/tmp/$image->{id}.jpg" or die $!;
print $fh $data;
close $fh;
},
interval => 3,
timeout => 60,
);
=head1 DESCRIPTION
Official Perl SDK for the L<Apertur|https://apertur.ca> API. Supports
API key and OAuth token authentication, session management, image
uploads (plain and encrypted), long polling, webhook signature
verification, and full resource CRUD for destinations, API keys,
webhooks, and encryption keys.
=head1 CONSTRUCTOR
=over 4
=item B<new(%args)>
Creates a new Apertur SDK client. At least one of C<api_key> or
lib/Apertur/SDK.pm view on Meta::CPAN
sandbox URL C<https://sandbox.api.aptr.ca>.
=back
=head1 RESOURCE ACCESSORS
=over 4
=item B<sessions> - L<Apertur::SDK::Resource::Sessions>
=item B<upload> - L<Apertur::SDK::Resource::Upload>
=item B<uploads> - L<Apertur::SDK::Resource::Uploads>
=item B<polling> - L<Apertur::SDK::Resource::Polling>
=item B<destinations> - L<Apertur::SDK::Resource::Destinations>
=item B<keys> - L<Apertur::SDK::Resource::Keys>
=item B<webhooks> - L<Apertur::SDK::Resource::Webhooks>
=item B<encryption> - L<Apertur::SDK::Resource::Encryption>
lib/Apertur/SDK/Crypto.pm view on Meta::CPAN
algorithm => 'RSA-OAEP+AES-256-GCM',
};
}
1;
__END__
=head1 NAME
Apertur::SDK::Crypto - Image encryption for Apertur uploads
=head1 SYNOPSIS
use Apertur::SDK::Crypto qw(encrypt_image);
my $result = encrypt_image($image_bytes, $public_key_pem);
# $result->{encrypted_key} - Base64-encoded RSA-wrapped AES key
# $result->{iv} - Base64-encoded 12-byte IV
# $result->{encrypted_data} - Base64-encoded AES-256-GCM ciphertext + tag
# $result->{algorithm} - "RSA-OAEP+AES-256-GCM"
lib/Apertur/SDK/HTTPClient.pm view on Meta::CPAN
my ($self, $method, $path, %opts) = @_;
my $url = $self->{base_url} . $path;
my $headers = $opts{headers} || {};
$headers->{'Authorization'} = $self->{auth_header}
if $self->{auth_header};
my $req;
if ($opts{multipart}) {
# Multipart form upload
$req = POST(
$url,
Content_Type => 'form-data',
Content => $opts{multipart},
);
# Apply auth header on top of the multipart request
$req->header('Authorization' => $headers->{'Authorization'})
if $headers->{'Authorization'};
# Apply any extra headers
for my $key (keys %$headers) {
lib/Apertur/SDK/HTTPClient.pm view on Meta::CPAN
=head1 SYNOPSIS
use Apertur::SDK::HTTPClient;
my $http = Apertur::SDK::HTTPClient->new(
base_url => 'https://api.aptr.ca',
api_key => 'aptr_live_...',
);
my $data = $http->request('GET', '/api/v1/stats');
my $raw = $http->request_raw('GET', '/api/v1/upload-sessions/uuid/qr');
=head1 DESCRIPTION
Low-level HTTP client used internally by all Apertur SDK resource classes.
Handles JSON serialisation, bearer token authentication, multipart uploads,
and maps HTTP error responses to typed exception objects.
=head1 METHODS
=over 4
=item B<new(%args)>
Constructor. Accepts C<base_url>, C<api_key>, and C<oauth_token>.
lib/Apertur/SDK/Resource/Destinations.pm view on Meta::CPAN
1;
__END__
=head1 NAME
Apertur::SDK::Resource::Destinations - Destination management
=head1 DESCRIPTION
Manages upload destinations (S3, webhook, long-poll queue, etc.)
within a project.
=head1 METHODS
=over 4
=item B<list($project_id)>
Lists all destinations for a project.
lib/Apertur/SDK/Resource/Encryption.pm view on Meta::CPAN
__END__
=head1 NAME
Apertur::SDK::Resource::Encryption - Encryption key retrieval
=head1 DESCRIPTION
Retrieves the server's RSA public key used for end-to-end encrypted
image uploads.
=head1 METHODS
=over 4
=item B<get_server_key()>
Returns a hashref containing the server's RSA public key in PEM format
(C<publicKey> field).
lib/Apertur/SDK/Resource/Polling.pm view on Meta::CPAN
use Time::HiRes qw(sleep time);
sub new {
my ($class, %args) = @_;
return bless { http => $args{http} }, $class;
}
sub list {
my ($self, $uuid) = @_;
return $self->{http}->request('GET', "/api/v1/upload-sessions/$uuid/poll");
}
sub download {
my ($self, $uuid, $image_id) = @_;
return $self->{http}->request_raw(
'GET', "/api/v1/upload-sessions/$uuid/images/$image_id",
);
}
sub ack {
my ($self, $uuid, $image_id) = @_;
return $self->{http}->request(
'POST', "/api/v1/upload-sessions/$uuid/images/$image_id/ack",
);
}
sub poll_and_process {
my ($self, $uuid, $handler, %options) = @_;
my $interval = $options{interval} || 3;
my $timeout = $options{timeout} || 0;
my $start = time();
lib/Apertur/SDK/Resource/Polling.pm view on Meta::CPAN
1;
__END__
=head1 NAME
Apertur::SDK::Resource::Polling - Long polling for new images
=head1 DESCRIPTION
Polls an upload session for new images, downloads them, and
acknowledges receipt to advance the queue.
=head1 METHODS
=over 4
=item B<list($uuid)>
Returns the current poll result for a session (hashref with C<images>).
lib/Apertur/SDK/Resource/Sessions.pm view on Meta::CPAN
use URI::Escape qw(uri_escape);
sub new {
my ($class, %args) = @_;
return bless { http => $args{http} }, $class;
}
sub create {
my ($self, %options) = @_;
return $self->{http}->request(
'POST', '/api/v1/upload-sessions',
body => encode_json(\%options),
);
}
sub get {
my ($self, $uuid) = @_;
return $self->{http}->request('GET', "/api/v1/upload/$uuid/session");
}
sub update {
my ($self, $uuid, %options) = @_;
return $self->{http}->request(
'PATCH', "/api/v1/upload-sessions/$uuid",
body => encode_json(\%options),
);
}
sub list {
my ($self, %params) = @_;
my $qs = _build_query_string(%params);
return $self->{http}->request('GET', "/api/v1/sessions$qs");
}
sub recent {
my ($self, %params) = @_;
my $qs = _build_query_string(%params);
return $self->{http}->request('GET', "/api/v1/sessions/recent$qs");
}
sub qr {
my ($self, $uuid, %options) = @_;
my $qs = _build_query_string(%options);
return $self->{http}->request_raw('GET', "/api/v1/upload-sessions/$uuid/qr$qs");
}
sub verify_password {
my ($self, $uuid, $password) = @_;
return $self->{http}->request(
'POST', "/api/v1/upload/$uuid/verify-password",
body => encode_json({ password => $password }),
);
}
sub delivery_status {
my ($self, $uuid, %opts) = @_;
my $path = "/api/v1/upload-sessions/$uuid/delivery-status";
my %req_opts;
if (defined $opts{poll_from}) {
$path .= '?pollFrom=' . uri_escape($opts{poll_from});
# Long-poll: server holds up to 5 min; give the request 6 min so the
# server releases first under the happy path.
$req_opts{timeout} = 360;
}
return $self->{http}->request('GET', $path, %req_opts);
}
lib/Apertur/SDK/Resource/Sessions.pm view on Meta::CPAN
1;
__END__
=head1 NAME
Apertur::SDK::Resource::Sessions - Upload session management
=head1 DESCRIPTION
Provides methods to create, retrieve, update, and list upload sessions,
as well as password verification, QR code generation, and delivery status
checking.
=head1 METHODS
=over 4
=item B<create(%options)>
Creates a new upload session. Returns the session hashref including C<uuid>.
=item B<get($uuid)>
Retrieves session details by UUID.
=item B<update($uuid, %options)>
Updates a session's settings.
=item B<list(%params)>
lib/Apertur/SDK/Resource/Upload.pm view on Meta::CPAN
if ($options{source}) {
push @multipart, source => $options{source};
}
my %headers;
if ($options{password}) {
$headers{'x-session-password'} = $options{password};
}
return $self->{http}->request(
'POST', "/api/v1/upload/$uuid/images",
multipart => \@multipart,
headers => \%headers,
);
}
sub image_encrypted {
my ($self, $uuid, $file, $public_key, %options) = @_;
my ($data, $filename);
if (ref $file eq 'SCALAR') {
lib/Apertur/SDK/Resource/Upload.pm view on Meta::CPAN
$filename = $options{filename} || basename($file);
open my $fh, '<:raw', $file
or die "Cannot open file '$file': $!\n";
local $/;
$data = <$fh>;
close $fh;
}
my $encrypted = encrypt_image($data, $public_key);
# The server's `default` encrypted mode reads a multipart upload, parses
# the file bytes as a JSON envelope (camelCase), then RSA-OAEP-SHA256
# decrypts. Send the envelope as a file part, mirroring the plain
# `image` multipart transport.
my $envelope = encode_json({
encryptedKey => $encrypted->{encrypted_key},
iv => $encrypted->{iv},
encryptedData => $encrypted->{encrypted_data},
algorithm => $encrypted->{algorithm},
});
lib/Apertur/SDK/Resource/Upload.pm view on Meta::CPAN
}
my %headers = (
'X-Aptr-Encrypted' => 'default',
);
if ($options{password}) {
$headers{'x-session-password'} = $options{password};
}
return $self->{http}->request(
'POST', "/api/v1/upload/$uuid/images",
multipart => \@multipart,
headers => \%headers,
);
}
1;
__END__
=head1 NAME
Apertur::SDK::Resource::Upload - Image upload operations
=head1 DESCRIPTION
Handles uploading images to an upload session, both plain multipart
and end-to-end encrypted.
=head1 METHODS
=over 4
=item B<image($uuid, $file, %options)>
Uploads an image via multipart POST. C<$file> can be a file path string
or a scalar reference containing raw bytes. Options: C<filename>,
C<mimeType> (or C<mime_type>), C<source>, C<password>.
=item B<image_encrypted($uuid, $file, $public_key, %options)>
Encrypts an image with AES-256-GCM (key wrapped with RSA-OAEP) and
uploads it as a JSON payload. Requires C<Crypt::OpenSSL::RSA> and
C<CryptX>. Options: same as C<image>.
=back
=cut
lib/Apertur/SDK/Resource/Uploads.pm view on Meta::CPAN
use URI::Escape qw(uri_escape);
sub new {
my ($class, %args) = @_;
return bless { http => $args{http} }, $class;
}
sub list {
my ($self, %params) = @_;
my $qs = _build_query_string(%params);
return $self->{http}->request('GET', "/api/v1/uploads$qs");
}
sub recent {
my ($self, %params) = @_;
my $qs = _build_query_string(%params);
return $self->{http}->request('GET', "/api/v1/uploads/recent$qs");
}
sub _build_query_string {
my (%params) = @_;
my @parts;
for my $key (sort keys %params) {
next unless defined $params{$key};
push @parts, uri_escape($key) . '=' . uri_escape($params{$key});
}
return @parts ? '?' . join('&', @parts) : '';
lib/Apertur/SDK/Resource/Uploads.pm view on Meta::CPAN
1;
__END__
=head1 NAME
Apertur::SDK::Resource::Uploads - Upload listing operations
=head1 DESCRIPTION
Lists and retrieves recent uploads across all sessions.
=head1 METHODS
=over 4
=item B<list(%params)>
Lists uploads with optional pagination (C<page>, C<pageSize>).
=item B<recent(%params)>
Returns recent uploads with optional C<limit>.
=back
=cut
t/01_client.t view on Meta::CPAN
like($@, qr/api_key or oauth_token/, 'dies without credentials');
# OAuth token constructor
my $oauth_client = Apertur::SDK->new(oauth_token => 'some_oauth_token');
isa_ok($oauth_client, 'Apertur::SDK');
is($oauth_client->env, 'live', 'non-prefixed oauth token defaults to live');
# --- Resource accessors ---
isa_ok($client->sessions, 'Apertur::SDK::Resource::Sessions');
isa_ok($client->upload, 'Apertur::SDK::Resource::Upload');
isa_ok($client->uploads, 'Apertur::SDK::Resource::Uploads');
isa_ok($client->polling, 'Apertur::SDK::Resource::Polling');
isa_ok($client->destinations, 'Apertur::SDK::Resource::Destinations');
isa_ok($client->keys, 'Apertur::SDK::Resource::Keys');
isa_ok($client->webhooks, 'Apertur::SDK::Resource::Webhooks');
isa_ok($client->encryption, 'Apertur::SDK::Resource::Encryption');
isa_ok($client->stats, 'Apertur::SDK::Resource::Stats');
# --- Error classes ---
subtest 'Error hierarchy' => sub {