MailSlurp logo

Extract invoice fields from PDF attachments

Documentation navigation
Search documentation

Receive a PDF invoice, extract typed fields and validate them before writing to your accounting system.

View MarkdownAgent setup

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. Send a representative invoice to it and wait for the matching email. 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 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.

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.

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();
  };
}
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.

POST /ai/structured-content/attachment

Generate structured content for an attachment

Use output schemas to extract data from an attachment using AI

Request, parameters, and responses

Request body (required)

FieldTypeRequiredDescription
attachmentIdstringYesAttachment ID to read and pass to AI
instructionsstringNoOptional instructions for the AI to follow. Try to be precise and clear. You can include examples and hints.
outputSchemaStructuredOutputSchemaNo
transformIdstring:uuidNoID of transformer to apply
emailIdstring:uuidNoOptional email ID for more context
Request example
{
  "attachmentId": "00000000-0000-4000-8000-000000000000",
  "instructions": "value",
  "outputSchema": {
    "anyOf": [
      {
        "anyOf": [
          "value"
        ],
        "default": {},
        "description": "value",
        "enumValues": [
          "value"
        ],
        "example": {},
        "format": "2026-06-21T00:00:00.000Z"
      }
    ],
    "default": {},
    "description": "value",
    "enumValues": [
      "value"
    ],
    "example": {},
    "format": "2026-06-21T00:00:00.000Z"
  },
  "transformId": "00000000-0000-4000-8000-000000000000",
  "emailId": "00000000-0000-4000-8000-000000000000"
}

Responses

StatusSchemaDescription
200StructuredContentResultDtoOK
HTTP and SDK snippets

HTTP

HTTP
POST /ai/structured-content/attachment HTTP/1.1
Host: api.mailslurp.com
x-api-key: YOUR_API_KEY
Accept: application/json
Content-Type: application/json

{
  "attachmentId": "00000000-0000-4000-8000-000000000000",
  "instructions": "value",
  "outputSchema": {
    "anyOf": [
      {
        "anyOf": [
          "value"
        ],
        "default": {},
        "description": "value",
        "enumValues": [
          "value"
        ],
        "example": {},
        "format": "2026-06-21T00:00:00.000Z"
      }
    ],
    "default": {},
    "description": "value",
    "enumValues": [
      "value"
    ],
    "example": {},
    "format": "2026-06-21T00:00:00.000Z"
  },
  "transformId": "00000000-0000-4000-8000-000000000000",
  "emailId": "00000000-0000-4000-8000-000000000000"
}

cURL

cURL
curl -X POST "https://api.mailslurp.com/ai/structured-content/attachment" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  --data '{"attachmentId":"00000000-0000-4000-8000-000000000000","instructions":"value","outputSchema":{"anyOf":[{"anyOf":["value"],"default":{},"description":"value","enumValues":["value"],"example":{},"format":"2026-06-21T00:00:00.000Z"}],"default":{},"description":"value","enumValues":["value"],"example":{},"format":"2026-06-21T00:00:00.000Z"},"transformId":"00000000-0000-4000-8000-000000000000","emailId":"00000000-0000-4000-8000-000000000000"}'

JavaScript SDK

JavaScript SDK
import { Configuration, AIControllerApi } from "mailslurp-client";

const config = new Configuration({ apiKey: "YOUR_API_KEY" });
const aIController = new AIControllerApi(config);
const request = {
  "generateStructuredContentAttachmentOptions": {
    "attachmentId": "00000000-0000-4000-8000-000000000000",
    "instructions": "value",
    "outputSchema": {
      "anyOf": [
        {
          "anyOf": [
            "value"
          ],
          "default": {},
          "description": "value",
          "enumValues": [
            "value"
          ],
          "example": {},
          "format": "2026-06-21T00:00:00.000Z"
        }
      ],
      "default": {},
      "description": "value",
      "enumValues": [
        "value"
      ],
      "example": {},
      "format": "2026-06-21T00:00:00.000Z"
    },
    "transformId": "00000000-0000-4000-8000-000000000000",
    "emailId": "00000000-0000-4000-8000-000000000000"
  }
};

const result = await aIController.generateStructuredContentFromAttachment(request);

Python SDK

Python SDK
import mailslurp_client
from mailslurp_client.api.ai_controller_api import AIControllerApi

configuration = mailslurp_client.Configuration()
configuration.api_key["x-api-key"] = "YOUR_API_KEY"

with mailslurp_client.ApiClient(configuration) as api_client:
    aIController = AIControllerApi(api_client)
    generate_structured_content_attachment_options = {
      "attachmentId": "00000000-0000-4000-8000-000000000000",
      "instructions": "value",
      "outputSchema": {
        "anyOf": [
          {
            "anyOf": [
              "value"
            ],
            "default": {},
            "description": "value",
            "enumValues": [
              "value"
            ],
            "example": {},
            "format": "2026-06-21T00:00:00.000Z"
          }
        ],
        "default": {},
        "description": "value",
        "enumValues": [
          "value"
        ],
        "example": {},
        "format": "2026-06-21T00:00:00.000Z"
      },
      "transformId": "00000000-0000-4000-8000-000000000000",
      "emailId": "00000000-0000-4000-8000-000000000000"
    }
    result = aIController.generate_structured_content_from_attachment(generate_structured_content_attachment_options)

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. Test it on known attachments through invocation before mapping it to an inbox. Consume stored results or receive result webhooks, deduplicating writes to your destination.

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