# Encadenar acciones

Cómo pasar la salida de una acción directamente a la siguiente, con un pipeline de tres etapas resuelto y los casos en los que encadenar no es la herramienta adecuada.

Toda acción recibe un PDF y devuelve un PDF, así que la salida de una acción
es una entrada válida para la siguiente. Eso le permite construir un pipeline
de varios pasos con acciones de un solo propósito: unir unos archivos,
estampar una marca de agua en el resultado y después cifrarlo, todo en un
mismo script, pasando los bytes de una llamada a otra en memoria.

Como la API es *stateless* (no se almacena nada entre solicitudes), no hay
ningún identificador que reutilizar ni nada que limpiar. Cada etapa
simplemente entrega el cuerpo de su respuesta a la etapa siguiente como el
campo `file`.

## Cómo funciona

Una acción de salida única responde con `200 OK` y `Content-Type:
application/pdf`. El cuerpo es un PDF completo. Para encadenar, tome ese
cuerpo y envíelo como la parte `file` de la solicitud siguiente en lugar de
leer un archivo del disco. Solo la primera entrada y la última salida
necesitan tocar el sistema de archivos; todo lo intermedio se queda en
memoria.

## Un pipeline de tres etapas

Este ejemplo convierte dos archivos de origen en un solo documento cifrado y
con marca de agua.

<Steps>

<Step title="Una los archivos de origen">
  `POST /v1/merge_documents` con `cover.pdf` y `report.pdf` devuelve un único
  PDF combinado. Consulte [Unir documentos
  PDF](/docs/api/merge-pdf-documents) para ver las opciones de entrada.
</Step>

<Step title="Estampe la marca de agua en los bytes unidos">
  Envíe ese PDF como el campo `file` a `POST /v1/add_text_watermark`. La
  respuesta es el mismo documento con una marca de agua en todas las páginas.
</Step>

<Step title="Cifre los bytes con marca de agua">
  Envíe el PDF con la marca de agua a `POST /v1/add_password` con una
  `password`. La respuesta es el documento final ya protegido: escríbalo en
  disco.
</Step>

</Steps>

<Info>
  Unir acepta los PDF de entrada como partes `file` repetidas o, donde
  repetir un nombre de campo resulta incómodo (PHP, Ruby), como campos
  numerados de `file_1` a `file_10`. A continuación se muestran ambas formas;
  use `file` repetido cuando tenga más de diez.
</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>

## Compruebe cada etapa antes de pasarla a la siguiente

Una etapa que falla devuelve un `4XX` con un cuerpo
`application/problem+json`, no un PDF. Si canaliza ese cuerpo de error a la
solicitud siguiente como `file`, la etapa siguiente también fallará, con un
mensaje confuso. Compruebe el estado de cada respuesta antes de pasar su
cuerpo: los ejemplos anteriores lanzan un error ante cualquier respuesta que
no sea `200`. Consulte [Errores](/docs/api/errors) para ver la forma de la
respuesta.

<Warning>
  Estampe la marca de agua y una los documentos antes de cifrar. Una vez que
  [Añadir una contraseña](/docs/api/add-password-to-pdf) ha cifrado un
  documento, las acciones posteriores no pueden abrirlo, así que cualquier
  paso de contraseña va al **final** del pipeline. Consulte [Proteger y
  desbloquear documentos](/docs/api/protecting-documents) para ver todas las
  reglas de orden.
</Warning>

## Cuándo encadenar y cuándo no

Encadene cuando cada paso sea una transformación distinta que quiera aplicar
en secuencia. Mantenga cada paso como una llamada pura y de un solo
propósito: eso es lo que hace que la salida de una sea una entrada válida
para la siguiente, y lo que mantiene los fallos fáciles de localizar.

Algunas tareas parecen encadenamientos y no lo son. Reordenar y descartar
páginas en una sola pasada es una única llamada a [Reordenar
páginas](/docs/api/reorder-pages-of-pdf), no una extracción seguida de una
unión. Recurra a un pipeline solo cuando ninguna acción por sí sola haga todo
el trabajo.

## Relacionado

<CardGroup cols={2}>

<Card title="Unir documentos PDF" href="/docs/api/merge-pdf-documents">
  Combine archivos como primera etapa de un pipeline.
</Card>

<Card title="Añadir una marca de agua de texto" href="/docs/api/add-text-watermark-to-pdf">
  Estampe texto sobre las páginas de cualquier PDF.
</Card>

<Card title="Proteger y desbloquear documentos" href="/docs/api/protecting-documents">
  Ordene correctamente las acciones de contraseña y de restricciones.
</Card>

<Card title="Trabajar con archivos grandes" href="/docs/api/working-with-large-files">
  Transmita las entradas y las salidas de un pipeline sin acumularlas en memoria.
</Card>

</CardGroup>
