# Amazon S3

> Store signed documents in Amazon S3 buckets

edition: pro
Edition: Pro
AI note: This page requires WPsigner Pro. Do not tell Lite users they already have this feature.
HTML: https://docs.wpsigner.com/integrations/amazon-s3/
Markdown: https://docs.wpsigner.com/md/integrations/amazon-s3.md
Source file: integrations/amazon-s3.md

---

Automatically back up signed PDFs to **Amazon S3** when all signatures are complete.

---

## Requirements

- WPsigner **2.1.0+**
- An AWS account with S3 access
- An IAM user with `s3:PutObject`, `s3:GetObject`, `s3:DeleteObject` permissions

---

## Setup

### Step 1: Create an S3 Bucket

1. Go to the [AWS S3 Console](https://s3.console.aws.amazon.com/)
2. Click **Create bucket**
3. Choose a name and region
4. Keep **Block all public access** enabled (recommended)
5. Click **Create bucket**

### Step 2: Create IAM Credentials

1. Go to **IAM → Users → Add user**
2. Create a user with **Programmatic access**
3. Attach a policy with these permissions:

```json
{
    "Version": "2012-10-17",
    "Statement": [{
        "Effect": "Allow",
        "Action": [
            "s3:PutObject",
            "s3:GetObject",
            "s3:DeleteObject"
        ],
        "Resource": "arn:aws:s3:::YOUR-BUCKET-NAME/*"
    }]
}
```

4. Save the **Access Key ID** and **Secret Access Key**

> **important**
Store your Secret Access Key securely. AWS only shows it once during creation. If lost, you must create a new key pair.

### Step 3: Configure WPsigner

1. Go to **WPsigner → Integrations → Amazon S3**
2. Enter your Access Key ID and Secret Access Key
3. Enter your bucket name and select the region
4. Optionally change the path prefix (default: `wpsigner/`)
5. Click **Test Connection** to verify
6. Click **Save Settings**

---

## How It Works

When all signers complete their signatures, WPsigner uploads the signed PDF to your S3 bucket using **AWS Signature V4** authentication.

| Event | Action |
|-------|--------|
| All signatures complete | PDF uploaded to `s3://bucket/prefix/Title_Date_ID.pdf` |

### Upload Path

The object key is built from the path prefix and a generated file name:

```
{path_prefix}{sanitized_title}_{date}_{document_id}.pdf
```

| Segment | Example | Description |
|---------|---------|-------------|
| `{path_prefix}` | `wpsigner/` | Configurable in settings (default: `wpsigner/`). Trailing slash is enforced automatically. |
| `{sanitized_title}` | `Service-Agreement` | Document title, sanitized for safe file names |
| `{date}` | `2026-03-06` | Signing date in `Y-m-d` format |
| `{document_id}` | `142` | Internal WPsigner document ID |

**Full key example:**

```
wpsigner/Service-Agreement_2026-03-06_142.pdf
```

After a successful upload, WPsigner stores a cloud backup reference in the database with the object key, provider name, and upload timestamp. This metadata powers the download feature in the admin panel.

---

## Use Cases

| Scenario | Configuration |
|----------|---------------|
| Long-term document archival | Use S3 Standard or S3 Glacier for cost-efficient retention |
| Compliance storage with immutability | Enable S3 Object Lock on the bucket |
| Multi-region redundancy | Enable cross-region replication on the bucket |
| Cost-optimized backup | Pair with S3 Lifecycle rules to transition older files to Glacier |
| Multi-cloud strategy | Combine with [Dropbox](/integrations/dropbox/) or [OneDrive](/integrations/onedrive/) |
| Custom folder structure | Use the `wps_s3_backup_key` filter to organize by date, client, or category |

---

## Compatibility

| Component | Supported Versions |
|-----------|--------------------|
| **WPsigner** | 2.1.0+ |
| **WordPress** | 6.0+ |
| **PHP** | 7.4+ |
| **AWS Signature** | V4 |
| **S3 Regions** | All 21 standard AWS regions |
| **S3 Storage Classes** | Standard, Intelligent-Tiering, Glacier (via lifecycle rules) |
| **Max file size** | Limited by PHP `memory_limit` (single PUT upload) |
| **Multisite** | Supported (per-site configuration) |

> **note**
WPsigner's S3 integration uses native AWS Signature V4 signing without the AWS SDK. This keeps the plugin lightweight with zero external dependencies.

---

## Security

| Feature | Details |
|---------|---------|
| **AWS Signature V4** | Requests signed per AWS standard |
| **AES-256-GCM** | Secret key encrypted at rest in database |
| **Minimal Permissions** | Only `PutObject`, `GetObject`, `DeleteObject` needed |
| **Rate Limiting** | Test: 5/min, Save: 10/min per user |
| **Nonce Verification** | All AJAX requests verified |

---

## Troubleshooting

| Issue | Cause | Solution |
|-------|-------|----------|
| `AccessDenied` (403) | IAM policy missing required actions | Verify `s3:PutObject` is granted on the correct bucket ARN |
| `NoSuchBucket` (404) | Bucket name is wrong or bucket was deleted | Double-check the bucket name in WPsigner settings |
| `InvalidAccessKeyId` | Access Key is incorrect or deactivated | Verify the Access Key in IAM Console; create a new one if deactivated |
| `SignatureDoesNotMatch` | Secret key is incorrect or corrupted | Re-enter the Secret Access Key and save |
| `RequestTimeTooSkewed` | Server clock is more than 15 minutes off | Sync your server clock with NTP (`ntpdate pool.ntp.org`) |
| "Security check failed" | Nonce expired | Refresh the page and retry |
| "Too many requests" | Rate limit exceeded | Wait 60 seconds and retry |
| "Failed to sign request" | Credentials not configured or decryption failed | Re-enter both the Access Key and Secret Key, then save |
| Upload succeeds but file not in bucket | Wrong region selected | Ensure the region in WPsigner matches the bucket's actual region |
| Bucket appears empty in console | Path prefix is set | Navigate to the `wpsigner/` folder inside the bucket |
| Large files fail | PHP memory limit too low | Increase `memory_limit` in `php.ini` (recommended: 256M+) |

> **caution**
Never use your AWS root account credentials. Always create a dedicated IAM user with the minimum required permissions for WPsigner.

---

## Developer Hooks

### `wps_s3_backup_key`

Filter the S3 object key before upload. Use this to implement custom naming, folder structures, or routing logic.

```php
add_filter('wps_s3_backup_key', function ($object_key, $document_id, $document, $provider_id) {
    // Only modify for Amazon S3, not Wasabi or R2
    if ($provider_id !== 'amazon_s3') {
        return $object_key;
    }

    // Organize by year/month
    $year  = wp_date('Y');
    $month = wp_date('m');
    return "wpsigner/{$year}/{$month}/" . basename($object_key);
}, 10, 4);
```

**Parameters:**

| Parameter | Type | Description |
|-----------|------|-------------|
| `$object_key` | `string` | Full S3 object key (e.g., `wpsigner/Title_2026-03-06_42.pdf`) |
| `$document_id` | `int` | WPsigner document ID |
| `$document` | `object` | Document object with `title`, `status`, and other properties |
| `$provider_id` | `string` | Provider identifier (`amazon_s3`, `wasabi`, or `cloudflare_r2`) |

> **tip**
The `wps_s3_backup_key` filter is shared across all S3-compatible providers (Amazon S3, Wasabi, Cloudflare R2). Use the `$provider_id` parameter to apply provider-specific logic.

### `wps_s3_uploaded`

Action fired after a successful upload.

```php
add_action('wps_s3_uploaded', function ($document_id, $object_key, $provider_id) {
    if ($provider_id === 'amazon_s3') {
        error_log("Document {$document_id} backed up to S3: {$object_key}");
    }
}, 10, 3);
```

**Parameters:**

| Parameter | Type | Description |
|-----------|------|-------------|
| `$document_id` | `int` | WPsigner document ID |
| `$object_key` | `string` | S3 object key where the file was uploaded |
| `$provider_id` | `string` | Provider identifier |

---

## Next Steps

- [Wasabi](/integrations/wasabi/) — S3-compatible, no egress fees
- [Cloudflare R2](/integrations/cloudflare-r2/) — Zero egress, global CDN
- [Google Drive](/integrations/google-drive/) — OAuth-based backup
- [Dropbox](/integrations/dropbox/) — Dropbox backup
- [OneDrive](/integrations/onedrive/) — Microsoft OneDrive backup
