# Aktionen verketten

Wie Sie die Ausgabe einer Aktion direkt an die nächste übergeben, mit einer ausgearbeiteten dreistufigen Pipeline und den Fällen, in denen Verketten das falsche Werkzeug ist.

Jede Aktion nimmt ein PDF als Eingabe entgegen und gibt ein PDF als Ausgabe
zurück, sodass die Ausgabe einer Aktion eine gültige Eingabe für die nächste
ist. Damit bauen Sie aus Aktionen mit einem einzigen Zweck eine mehrstufige
Pipeline: ein paar Dateien zusammenführen, auf dem Ergebnis ein Wasserzeichen
aufbringen, es dann verschlüsseln, alles in einem einzigen Skript, wobei die
Bytes von Aufruf zu Aufruf im Speicher bleiben.

Weil die API *stateless* ist (zwischen Anfragen wird nichts gespeichert), gibt
es kein Handle zum Wiederverwenden und nichts aufzuräumen. Jede Stufe übergibt
einfach ihren Antworttext im Feld `file` an die nächste Stufe.

## So funktioniert es

Eine Aktion mit einer einzigen Ausgabe antwortet mit `200 OK` und
`Content-Type: application/pdf`. Der Antworttext ist ein vollständiges PDF. Zum
Verketten nehmen Sie diesen Text und senden ihn als Teil `file` der nächsten
Anfrage, statt eine Datei von der Festplatte zu lesen. Nur die allererste
Eingabe und die allerletzte Ausgabe müssen das Dateisystem berühren; alles
dazwischen bleibt im Speicher.

## Eine dreistufige Pipeline

Dieses Beispiel macht aus zwei Quelldateien ein einziges verschlüsseltes
Dokument mit Wasserzeichen.

<Steps>

<Step title="Die Quelldateien zusammenführen">
  `POST /v1/merge_documents` mit `cover.pdf` und `report.pdf` gibt ein einziges
  kombiniertes PDF zurück. Die Eingabeoptionen finden Sie unter [Dokumente
  zusammenführen](/docs/api/merge-pdf-documents).
</Step>

<Step title="Ein Wasserzeichen auf die zusammengeführten Bytes aufbringen">
  Senden Sie dieses PDF im Feld `file` an `POST /v1/add_text_watermark`. Die
  Antwort ist dasselbe Dokument mit einem Wasserzeichen auf jeder Seite.
</Step>

<Step title="Die Bytes mit Wasserzeichen verschlüsseln">
  Senden Sie das PDF mit Wasserzeichen an `POST /v1/add_password` mit einem
  `password`. Die Antwort ist das fertige, geschützte Dokument. Schreiben Sie
  es auf die Festplatte.
</Step>

</Steps>

<Info>
  Das Zusammenführen nimmt die Eingabe-PDFs als wiederholte Teile `file`
  entgegen oder, wo das Wiederholen eines Feldnamens unhandlich ist (PHP,
  Ruby), als nummerierte Felder `file_1` bis `file_10`. Beides ist unten
  gezeigt; verwenden Sie das wiederholte `file`, wenn Sie mehr als zehn haben.
</Info>

<CodeGroup>

```bash title="cURL"
# Each stage reads the previous PDF from stdin via `-F 'file=@-'`.
curl -sS https://api.pdfblocks.com/v1/merge_documents \
  -H 'X-API-Key: your_api_key' \
  -F file=@cover.pdf \
  -F file=@report.pdf |
curl -sS https://api.pdfblocks.com/v1/add_text_watermark \
  -H 'X-API-Key: your_api_key' \
  -F 'file=@-;filename=merged.pdf;type=application/pdf' \
  -F line_1='CONFIDENTIAL' \
  -F line_2='ACME, Inc.' |
curl -sS https://api.pdfblocks.com/v1/add_password \
  -H 'X-API-Key: your_api_key' \
  -F 'file=@-;filename=watermarked.pdf;type=application/pdf' \
  -F password='pa$$word' \
  -o final.pdf
```

```python title="Python"
# pip install requests
import requests

BASE = 'https://api.pdfblocks.com'
HEADERS = {'X-API-Key': 'your_api_key'}

# 1. Merge cover.pdf and report.pdf into one document.
with open('cover.pdf', 'rb') as cover, open('report.pdf', 'rb') as report:
    merged = requests.post(
        f'{BASE}/v1/merge_documents',
        headers=HEADERS,
        files=[('file', cover), ('file', report)],
    )
merged.raise_for_status()

# 2. Watermark the merged bytes: no temp file.
watermarked = requests.post(
    f'{BASE}/v1/add_text_watermark',
    headers=HEADERS,
    files={'file': ('merged.pdf', merged.content, 'application/pdf')},
    data={'line_1': 'CONFIDENTIAL', 'line_2': 'ACME, Inc.'},
)
watermarked.raise_for_status()

# 3. Encrypt the watermarked bytes.
final = requests.post(
    f'{BASE}/v1/add_password',
    headers=HEADERS,
    files={'file': ('watermarked.pdf', watermarked.content, 'application/pdf')},
    data={'password': 'pa$$word'},
)
final.raise_for_status()

with open('final.pdf', 'wb') as output:
    output.write(final.content)
```

```javascript title="Node.js"
// Node.js 18+
import { readFile, writeFile } from 'node:fs/promises';

const BASE = 'https://api.pdfblocks.com';
const headers = { 'X-API-Key': 'your_api_key' };

// Post a multipart form and return the response PDF as bytes.
async function post(path, form) {
  const response = await fetch(BASE + path, { method: 'POST', headers, body: form });
  if (!response.ok) throw new Error(`${path} failed: ${response.status}`);
  return new Uint8Array(await response.arrayBuffer());
}

// 1. Merge cover.pdf and report.pdf.
const mergeForm = new FormData();
mergeForm.append('file', new Blob([await readFile('cover.pdf')]), 'cover.pdf');
mergeForm.append('file', new Blob([await readFile('report.pdf')]), 'report.pdf');
const merged = await post('/v1/merge_documents', mergeForm);

// 2. Watermark the merged bytes.
const watermarkForm = new FormData();
watermarkForm.append('file', new Blob([merged]), 'merged.pdf');
watermarkForm.append('line_1', 'CONFIDENTIAL');
watermarkForm.append('line_2', 'ACME, Inc.');
const watermarked = await post('/v1/add_text_watermark', watermarkForm);

// 3. Encrypt the watermarked bytes.
const passwordForm = new FormData();
passwordForm.append('file', new Blob([watermarked]), 'watermarked.pdf');
passwordForm.append('password', 'pa$$word');
const final = await post('/v1/add_password', passwordForm);

await writeFile('final.pdf', final);
```

```php title="PHP"
<?php
$base = 'https://api.pdfblocks.com';
$headers = ['X-API-Key: your_api_key'];

// Post a multipart form and return the response body, or throw on error.
function post(string $url, array $headers, array $fields): string {
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => $headers,
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => $fields,
    ]);
    $body = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    if ($status !== 200) {
        throw new RuntimeException("Request to $url failed with status $status.");
    }
    return $body;
}

// 1. Merge cover.pdf and report.pdf (numbered fields for the array input).
$merged = post("$base/v1/merge_documents", $headers, [
    'file_1' => new CURLFile('cover.pdf', 'application/pdf'),
    'file_2' => new CURLFile('report.pdf', 'application/pdf'),
]);

// 2. Watermark the merged bytes in memory (CURLStringFile, PHP 8.1+).
$watermarked = post("$base/v1/add_text_watermark", $headers, [
    'file' => new CURLStringFile($merged, 'merged.pdf', 'application/pdf'),
    'line_1' => 'CONFIDENTIAL',
    'line_2' => 'ACME, Inc.',
]);

// 3. Encrypt the watermarked bytes.
$final = post("$base/v1/add_password", $headers, [
    'file' => new CURLStringFile($watermarked, 'watermarked.pdf', 'application/pdf'),
    'password' => 'pa$$word',
]);

file_put_contents('final.pdf', $final);
```

```ruby title="Ruby"
# gem install http
require 'http'
require 'stringio'

BASE = 'https://api.pdfblocks.com'
HEADERS = { 'X-API-Key' => 'your_api_key' }

def post(path, fields)
  response = HTTP.headers(HEADERS).post("#{BASE}#{path}", form: fields)
  raise "#{path} failed: #{response.status}" unless response.status.success?
  response.body.to_s
end

# 1. Merge cover.pdf and report.pdf (numbered fields for the array input).
merged = post('/v1/merge_documents',
  file_1: HTTP::FormData::File.new('cover.pdf'),
  file_2: HTTP::FormData::File.new('report.pdf'))

# 2. Watermark the merged bytes in memory.
watermarked = post('/v1/add_text_watermark',
  file: HTTP::FormData::File.new(StringIO.new(merged),
    filename: 'merged.pdf', content_type: 'application/pdf'),
  line_1: 'CONFIDENTIAL',
  line_2: 'ACME, Inc.')

# 3. Encrypt the watermarked bytes.
final = post('/v1/add_password',
  file: HTTP::FormData::File.new(StringIO.new(watermarked),
    filename: 'watermarked.pdf', content_type: 'application/pdf'),
  password: 'pa$$word')

File.write('final.pdf', final)
```

```go title="Go"
package main

import (
	"bytes"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"os"
)

const base = "https://api.pdfblocks.com"

// post sends a multipart form and returns the response PDF bytes.
func post(path string, build func(*multipart.Writer)) []byte {
	var buf bytes.Buffer
	form := multipart.NewWriter(&buf)
	build(form)
	form.Close()

	req, _ := http.NewRequest("POST", base+path, &buf)
	req.Header.Set("Content-Type", form.FormDataContentType())
	req.Header.Set("X-API-Key", "your_api_key")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	body, _ := io.ReadAll(res.Body)
	if res.StatusCode != http.StatusOK {
		panic(fmt.Sprintf("%s failed: %d", path, res.StatusCode))
	}
	return body
}

func addFile(form *multipart.Writer, name string, data []byte) {
	part, _ := form.CreateFormFile("file", name)
	part.Write(data)
}

func main() {
	cover, _ := os.ReadFile("cover.pdf")
	report, _ := os.ReadFile("report.pdf")

	// 1. Merge cover.pdf and report.pdf.
	merged := post("/v1/merge_documents", func(form *multipart.Writer) {
		addFile(form, "cover.pdf", cover)
		addFile(form, "report.pdf", report)
	})

	// 2. Watermark the merged bytes.
	watermarked := post("/v1/add_text_watermark", func(form *multipart.Writer) {
		addFile(form, "merged.pdf", merged)
		form.WriteField("line_1", "CONFIDENTIAL")
		form.WriteField("line_2", "ACME, Inc.")
	})

	// 3. Encrypt the watermarked bytes.
	final := post("/v1/add_password", func(form *multipart.Writer) {
		addFile(form, "watermarked.pdf", watermarked)
		form.WriteField("password", "pa$$word")
	})

	os.WriteFile("final.pdf", final, 0644)
}
```

```csharp title="C#"
using System.Net.Http.Headers;

const string Base = "https://api.pdfblocks.com";

using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", "your_api_key");

// Post a multipart form and return the response PDF bytes.
async Task<byte[]> Post(string path, Action<MultipartFormDataContent> build)
{
    using var form = new MultipartFormDataContent();
    build(form);
    var response = await client.PostAsync(Base + path, form);
    response.EnsureSuccessStatusCode();
    return await response.Content.ReadAsByteArrayAsync();
}

static void AddFile(MultipartFormDataContent form, string name, byte[] data)
{
    var content = new ByteArrayContent(data);
    content.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
    form.Add(content, "file", name);
}

// 1. Merge cover.pdf and report.pdf.
var merged = await Post("/v1/merge_documents", form =>
{
    AddFile(form, "cover.pdf", File.ReadAllBytes("cover.pdf"));
    AddFile(form, "report.pdf", File.ReadAllBytes("report.pdf"));
});

// 2. Watermark the merged bytes.
var watermarked = await Post("/v1/add_text_watermark", form =>
{
    AddFile(form, "merged.pdf", merged);
    form.Add(new StringContent("CONFIDENTIAL"), "line_1");
    form.Add(new StringContent("ACME, Inc."), "line_2");
});

// 3. Encrypt the watermarked bytes.
var final = await Post("/v1/add_password", form =>
{
    AddFile(form, "watermarked.pdf", watermarked);
    form.Add(new StringContent("pa$$word"), "password");
});

await File.WriteAllBytesAsync("final.pdf", final);
```

</CodeGroup>

## Jede Stufe prüfen, bevor sie weitergereicht wird

Eine fehlgeschlagene Stufe gibt einen `4XX` mit einem Antworttext vom Typ
`application/problem+json` zurück, kein PDF. Wenn Sie diesen Fehlertext als
`file` in die nächste Anfrage leiten, schlägt auch die nächste Stufe fehl, mit
einer verwirrenden Meldung. Prüfen Sie den Status jeder Antwort, bevor Sie
ihren Text weiterreichen: Die Beispiele oben lösen bei allem außer `200` einen
Fehler aus. Die Form der Antwort finden Sie unter
[Fehler](/docs/api/errors).

<Warning>
  Bringen Sie Wasserzeichen auf und führen Sie zusammen, bevor Sie
  verschlüsseln. Sobald [Passwort hinzufügen](/docs/api/add-password-to-pdf)
  ein Dokument verschlüsselt hat, können spätere Aktionen es nicht mehr öffnen;
  jeder Schritt mit einem Passwort gehört daher ans **Ende** der Pipeline. Alle
  Regeln zur Reihenfolge finden Sie unter [Dokumente schützen und
  entsperren](/docs/api/protecting-documents).
</Warning>

## Wann Sie verketten sollten und wann nicht

Verketten Sie, wenn jeder Schritt eine eigenständige Transformation ist, die
Sie der Reihe nach anwenden wollen. Jeder Schritt sollte ein reiner
Aufruf mit einem einzigen Zweck bleiben: Genau das macht die Ausgabe des einen
zu einer gültigen Eingabe für den nächsten, und genau das sorgt dafür, dass
sich Fehler leicht eingrenzen lassen.

Manche Aufgaben sehen nach Verketten aus, sind es aber nicht. Seiten in einem
Durchgang neu anzuordnen und dabei welche zu verwerfen, ist ein einziger Aufruf
von [Seiten neu anordnen](/docs/api/reorder-pages-of-pdf), kein Extrahieren mit
anschließendem Zusammenführen. Greifen Sie erst dann zu einer Pipeline, wenn
keine einzelne Aktion die ganze Arbeit erledigt.

## Siehe auch

<CardGroup cols={2}>

<Card title="Dokumente zusammenführen" href="/docs/api/merge-pdf-documents">
  Dateien als erste Stufe einer Pipeline kombinieren.
</Card>

<Card title="Textwasserzeichen hinzufügen" href="/docs/api/add-text-watermark-to-pdf">
  Text über die Seiten eines beliebigen PDFs aufbringen.
</Card>

<Card title="Dokumente schützen und entsperren" href="/docs/api/protecting-documents">
  Die Aktionen für Passwörter und Einschränkungen richtig anordnen.
</Card>

<Card title="Mit großen Dateien arbeiten" href="/docs/api/working-with-large-files">
  Eingaben und Ausgaben einer Pipeline als Stream verarbeiten, ohne zu puffern.
</Card>

</CardGroup>
