> ## Documentation Index
> Fetch the complete documentation index at: https://www.integrate.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# ETL: PGP Keys

> Store OpenPGP key material once at the account level and reference it by id from any File Storage source or destination to decrypt inbound files or encrypt outbound files.

## Overview

PGP keys are account-level entries that hold OpenPGP key material. A File Storage source can reference an entry to decrypt inbound files, and a File Storage destination can reference an entry to encrypt outbound files. You paste the key material once in Account Settings instead of embedding it in each component.

Use a PGP key entry when:

* A trading partner encrypts files to your public key and drops them in a bucket you read from.
* You send files to a trading partner and must encrypt them with the partner's public key first.

## How it works

An entry is a key **ring**. It holds one or both directions:

| Field                                   | Direction | What it does                                        |
| :-------------------------------------- | :-------- | :-------------------------------------------------- |
| `private_key` (+ optional `passphrase`) | Decrypt   | Unlocks inbound files encrypted to your public key. |
| `public_key`                            | Encrypt   | Encrypts outbound files to a trading partner.       |

At least one of `private_key` or `public_key` must be set. An entry with only a private key can decrypt but not encrypt. An entry with only a public key can encrypt but not decrypt. An entry with both can do either, in different components.

Integrate.io reads the private key blob as a ring and matches each file to the key it was encrypted to. You can paste current plus retired private keys into one entry and still read archived files. Use separate entries only when the passphrases differ.

### What the entry stores

* **Name, description, status.** `status` is `active` or `disabled`. A disabled entry cannot be referenced by a component.
* **Key IDs.** Derived from the pasted material and shown in the UI so you can tell entries apart. A key ID is not secret. It is printed in the header of every file encrypted to that key.
* **Metadata about each key in the ring.** Key ID, fingerprint, algorithm, bit length, creation timestamp, and whether the key is a primary or subkey.

### Write-only key material

`private_key` and `passphrase` are write-only. Once saved, the API never returns them, not even masked. The response carries `has_private_key`, `has_passphrase`, `can_decrypt`, and `can_encrypt` booleans instead.

`public_key` is returned in full, because it is meant to be handed to trading partners.

### Rotation and clusters

The list of decrypt-capable keys is loaded onto a cluster when the cluster is created. Keys created or rotated **after** the cluster has started are not visible to jobs already running on it. If you rotate a key and a job cannot find it, start the job on a new cluster.

## Permissions

| Role                  | View | Add / Update / Delete |
| :-------------------- | :--- | :-------------------- |
| Owner                 | Yes  | Yes                   |
| Admin                 | Yes  | Yes                   |
| Operator              | Yes  | Yes                   |
| Member                | Yes  | No                    |
| Editor                | Yes  | No                    |
| Collaborator / Reader | No   | No                    |

Read is granted broadly because listing keys is how a package author picks one on a file component. Write is narrower because replacing a key silently breaks every pipeline that decrypts with it.

## Configuration

### Adding a PGP key

<Steps>
  <Step>Go to **Account Settings -> PGP Keys**.</Step>
  <Step>Click **Add PGP Key**.</Step>
  <Step>Enter a **Name** and optional **Description**.</Step>
  <Step>Paste an armored **Private key** block, an armored **Public key** block, or both. Include the `-----BEGIN PGP ... KEY BLOCK-----` and `-----END PGP ... KEY BLOCK-----` lines.</Step>
  <Step>If the private key is protected, enter the **Passphrase**.</Step>
  <Step>Click **Save**. The entry is validated as a parseable OpenPGP block before it saves. A public key pasted into the private key field is rejected.</Step>
</Steps>

### Using a PGP key on a File Storage source

Open a File Storage source, select your file storage connection, and pick a PGP key from the **PGP Key** dropdown. The entry must have a private key. At runtime, Integrate.io decrypts each file under the source path with a matching key from that ring before parsing it.

The source path is used to scope the key to this component. Paths with a space, semicolon, or parenthesis are rejected at save time, because the runtime cannot express those in a key scope.

### Using a PGP key on a File Storage destination

Open a File Storage destination, select your file storage connection, and pick a PGP key from the **PGP Key** dropdown. The entry must have a public key. At runtime, output files are encrypted to that public key before they are written to the target path.

Package validation refuses a destination that references a disabled entry, an entry that does not exist, or an entry with no public key. The alternative is a job that succeeds and ships a partner cleartext.

## API

### Endpoints

| Method   | Path                              | Purpose                                       |
| :------- | :-------------------------------- | :-------------------------------------------- |
| `GET`    | `/{account_id}/api/pgp_keys`      | List entries. Add `?status=active` to filter. |
| `GET`    | `/{account_id}/api/pgp_keys/{id}` | Show one entry.                               |
| `POST`   | `/{account_id}/api/pgp_keys`      | Create an entry.                              |
| `PUT`    | `/{account_id}/api/pgp_keys/{id}` | Update an entry.                              |
| `DELETE` | `/{account_id}/api/pgp_keys/{id}` | Delete an entry.                              |

### Fields

Request body:

* `name` (required, up to 255 characters, unique per account, case-insensitive)
* `description` (up to 1024 characters)
* `status` (`active` or `disabled`, defaults to `active`)
* `private_key` (armored PGP private key block)
* `passphrase` (matches `private_key`)
* `public_key` (armored PGP public key block)

Response body:

* `id`, `account_id`, `owner_id`, `name`, `description`, `status`
* `public_key` (returned in full)
* `has_private_key`, `has_passphrase`, `can_decrypt`, `can_encrypt`
* `key_ids`: comma-separated list of key IDs in the ring
* `keys`: array of `{ key_id, fingerprint, algorithm, bits, created_at, primary }` for each key in the ring
* `user_ids`: user IDs embedded in the key material
* `owner`: `{ id, name, email }`
* `created_at`, `updated_at`

`private_key` and `passphrase` are never returned.

### Create example

```bash theme={null}
curl -X POST "https://api.integrate.io/{account_id}/api/pgp_keys" \
  -u "{api_key}:" \
  -H "Content-Type: application/json" \
  -d '{
    "pgp_key": {
      "name": "Acme partner",
      "description": "Ring holding Acme public key and our own private key.",
      "public_key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...\n-----END PGP PUBLIC KEY BLOCK-----",
      "private_key": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n...\n-----END PGP PRIVATE KEY BLOCK-----",
      "passphrase": "s3cret"
    }
  }'
```

### Update example

Updates are partial. Omitting `private_key`, `passphrase`, or `public_key` leaves the stored value unchanged. Sending an explicitly empty string clears it, subject to the "at least one of private or public must be set" rule.

```bash theme={null}
curl -X PUT "https://api.integrate.io/{account_id}/api/pgp_keys/{id}" \
  -u "{api_key}:" \
  -H "Content-Type: application/json" \
  -d '{
    "pgp_key": {
      "status": "disabled"
    }
  }'
```

## FAQ

**Q: Can I retrieve a private key after saving it?**

No. The private key and passphrase are write-only. To replace them, submit a new value.

**Q: What happens if I rotate a key while a job is running?**

A running cluster loads decrypt-capable keys at creation time. A key created or rotated after the cluster started is not visible to jobs on it. Run the job on a new cluster, or paste the retired key alongside the current one in the same entry so the ring covers both.

**Q: Do I need separate entries for each trading partner?**

For decryption, no. Partners encrypt to your public key, so one private-key entry can decrypt files from any partner that uses it. For encryption, yes: each partner has its own public key, so each partner needs its own entry.

**Q: Can I use the same entry for both encryption and decryption?**

Yes, if it holds both a private key and a public key. The direction is decided by the component that references it, not by the entry itself.

**Q: Why was my key rejected at save time?**

The pasted block was not a parseable OpenPGP key, was over 1 MB, or was a public block pasted into the private field. The validator does not need the passphrase, so a correct private key with a wrong or missing passphrase still saves.

**Q: Why does my package fail to validate after I add a PGP key?**

Look at the error on the file component. Common causes are a disabled key, an entry that does not have the direction the component needs (a decrypt on an entry with no private key, or an encrypt on an entry with no public key), or a path with a space, semicolon, or parenthesis in it.

## Related

<CardGroup cols={2}>
  <Card title="ETL: File Storage Source" icon="arrow-right" href="/docs/etl/using-components-file-storage-source" horizontal />

  <Card title="ETL: File Storage Destination" icon="arrow-right" href="/docs/etl/using-components-file-storage-destination" horizontal />

  <Card title="ETL: Global Secrets" icon="arrow-right" href="/docs/etl/global-secrets-account-level" horizontal />

  <Card title="ETL: Encrypting and Decrypting Data" icon="arrow-right" href="/docs/etl/how-do-i-encrypt-and-decrypt-sensitive-data" horizontal />
</CardGroup>
