# Extract invoice fields from PDF attachments

Use attachment extraction when the invoice fields are inside a PDF or image rather than in the email body. Keep the source email and attachment IDs with the extracted values so a reviewer can inspect the document behind a record.

## Receive and select the attachment

Create an inbox or [connect your existing mailbox](/docs/connectors/). Send a representative invoice to it and [wait for the matching email](/docs/wait-for/). List the email's attachments and choose the invoice by its metadata; do not assume the first attachment is the invoice because signatures can contain inline images.

See [attachments](/docs/attachments/) for listing, downloading and inspecting files. Use the returned attachment ID, not its filename, in extraction requests.

## Define the fields

This schema keeps the invoice number as text so leading zeros survive. The total is numeric and its currency is a separate required field. Add line items or tax only when the downstream workflow needs them.

```javascript
const invoiceSchema = {
  type: 'object',
  required: ['invoiceNumber', 'supplier', 'currency', 'total'],
  properties: {
    invoiceNumber: {
      type: 'string',
      description: 'Invoice identifier exactly as printed, including leading zeros',
    },
    supplier: {
      type: 'string',
      description: 'Supplier or legal entity issuing the invoice',
    },
    purchaseOrderNumber: {
      type: 'string',
      nullable: true,
      description: 'Purchase order identifier when one appears on the invoice',
    },
    currency: {
      type: 'string',
      pattern: '^[A-Z]{3}$',
      description: 'Three-letter ISO currency code',
    },
    total: {
      type: 'number',
      minimum: 0,
      description: 'Final invoice total in the stated currency',
    },
  },
};
```

## Extract and validate

Use Node.js 22+ and set `MAILSLURP_API_KEY` on your server. The helper checks HTTP failures; the extraction function also checks the returned business fields.

```javascript
const API_BASE_URL = 'https://api.mailslurp.com';

function createApi(apiKey) {
  if (!apiKey) throw new Error('Set MAILSLURP_API_KEY');

  return async function api(path, { method = 'GET', body } = {}) {
    const response = await fetch(`${API_BASE_URL}${path}`, {
      method,
      headers: {
        'x-api-key': apiKey,
        ...(body ? { 'content-type': 'application/json' } : {}),
      },
      body: body ? JSON.stringify(body) : undefined,
    });

    if (!response.ok) {
      const detail = await response.text();
      throw new Error(`MailSlurp ${method} ${path} failed (${response.status}): ${detail}`);
    }

    return response.status === 204 ? null : response.json();
  };
}
```

```javascript
async function extractInvoice(api, attachmentId) {
  const response = await api('/ai/structured-content/attachment', {
    method: 'POST',
    body: {
      attachmentId,
      instructions: 'Extract only values present in the invoice. Do not infer missing fields.',
      outputSchema: invoiceSchema,
    },
  });

  const invoice = response.result;
  if (!invoice || typeof invoice !== 'object') throw new Error('No invoice data returned');
  if (typeof invoice.invoiceNumber !== 'string' || !invoice.invoiceNumber.trim()) {
    throw new Error('Missing invoice number');
  }
  if (typeof invoice.supplier !== 'string' || !invoice.supplier.trim()) {
    throw new Error('Missing supplier');
  }
  if (!/^[A-Z]{3}$/.test(invoice.currency)) throw new Error('Invalid currency');
  if (typeof invoice.total !== 'number' || !Number.isFinite(invoice.total)) {
    throw new Error('Invalid invoice total');
  }

  return invoice;
}
```

Call `extractInvoice(createApi(process.env.MAILSLURP_API_KEY), attachmentId)` after selecting the attachment. Compare the result with your expected supplier, purchase order, currency and permitted amounts before creating a payment or accounting entry. Schema validity alone does not establish that a document is genuine or that its values match your records.

[API endpoint: `generateStructuredContentFromAttachment`](/docs/api/#generateStructuredContentFromAttachment)

A missing, ambiguous, unreadable or unexpected value should enter a review path. Keep the original attachment accessible to the reviewer and avoid substituting zero or an empty string for missing financial data. Test representative invoices, scans, multiple attachments and non-invoice files before enabling automatic processing.

## Process future invoices

Save the schema and instructions as an [AI transformer](/docs/ai-transformers/). Test it on known attachments through [invocation](/docs/ai-invocation/) before mapping it to an inbox. Consume stored results or receive [result webhooks](/docs/ai-results/), deduplicating writes to your destination.

Message-testing AI endpoints have their own outcome and evidence contract. Use [match, assert and extract](/docs/ai/) for assertions about email or SMS text; use the attachment workflow here for file content.
