Skip to main content
Adding a new vendor requires two steps: creating a JSON config file and optionally adding a prompt config for the Gemini parser. No Lambda code changes are needed.
1

Create the vendor config JSON

Create a new file at vendor_configs/{sub_type}/{vendor_id}.json. The vendor_id must be lowercase and unique across all vendors — it is used as the DynamoDB partition key and as the S3 path segment.
{
  "vendor_id": {"S": "vendor_id"},
  "vendor_name": {"S": "Vendor Name"},
  "invoice_category": {"S": "retail"},
  "invoice_sub_type": {"S": "clothing"},
  "default_email_patterns": {"SS": ["noreply@vendor.com"]},
  "default_subject_keywords": {"SS": ["Your order"]},
  "parser_type": {"S": "html"},
  "active": {"BOOL": true},
  "supports_pdf": {"BOOL": false},
  "supports_html": {"BOOL": true},
  "logo_url": {"S": ""},
  "created_at": {"S": "2026-01-01T00:00:00Z"},
  "updated_at": {"S": "2026-01-01T00:00:00Z"}
}
Place the file in the subdirectory that matches the invoice_sub_type value (e.g. vendor_configs/clothing/ for clothing vendors).
2

Upload the config to DynamoDB

Run the upload script from the vendor_configs/ directory. The script iterates over all */*.json files and uploads each one using aws dynamodb put-item with a attribute_not_exists(vendor_id) condition, so existing records are never overwritten.
cd vendor_configs
./upload_vendors.sh
Example output:
=========================================
Uploading vendor configs to DynamoDB
Table: VendorConfig
=========================================

Uploading dominos.json ... SUCCESS
Uploading foodora.json ... ALREADY EXISTS
Uploading my_new_vendor.json ... SUCCESS

=========================================
Upload Summary
=========================================
Total files:      3
Successful:       2
Already existed:  1
Failed:           0
=========================================
The script exits with code 1 if any upload fails.
3

Add a prompt config (optional)

To improve parsing accuracy, add an entry to lambda_layers/gemini_parsers/python/vendor_prompt_configs/{sub_type}.py.
"vendor_id": {
    "vendor_desc": "Short description of the vendor",
    "is_email_in_swedish": False,
    "items_desc": "what the items are (e.g. electronics)",   # optional
    "header_info": 'Look for "Order confirmation" header.',   # optional
    "field_translations": {                                    # optional, Swedish vendors only
        "Ordernummer": "Order number",
        "Totalt": "Total",
    },
},
The vendor_id key must exactly match the vendor_id value in the JSON config. This step can be skipped — the parser will still run, but with less context.
4

Test the new vendor

Trigger a retail invoice fetch for your account by calling the ingest endpoint. You can scope the request to a specific date range to avoid re-fetching old data:
curl -X POST https://{api-id}.execute-api.eu-west-1.amazonaws.com/v1/invoices/retail/ingest \
  -H "Authorization: Bearer {your_jwt_token}" \
  -H "Content-Type: application/json" \
  -d '{
    "start_date": "2026-01-01",
    "end_date": "2026-03-21"
  }'
A successful response looks like:
{
  "message": "Retail invoices fetched successfully",
  "data": {
    "totalEmailsFound": 3,
    "byVendor": {
      "my_new_vendor": 3,
      "zalando": 0
    },
    "dateRange": {
      "start": "2026/01/01",
      "end": "2026/03/21"
    },
    "errors": []
  }
}
Check CloudWatch Logs at /aws/lambda/fetch_retail_invoices for per-email processing details and any errors.

Vendor config field reference

FieldDynamoDB typeRequiredDescription
vendor_idSYesUnique lowercase identifier. Used as DynamoDB PK and S3 path segment.
vendor_nameSYesHuman-readable display name shown in the iOS app.
invoice_categorySYesAlways retail.
invoice_sub_typeSYesOne of: clothing, food-delivery, food_delivery, miscellaneous, subscriptions, technology, travel, grocery, utility. Must match the subdirectory name.
default_email_patternsSSYesString Set of sender email addresses. At least one value is required — the vendor is skipped if this is empty.
default_subject_keywordsSSYesString Set of subject-line keywords used to narrow the Gmail search.
parser_typeSYesEmail format to parse. Use html.
activeBOOLYestrue to include in fetch runs, false to disable.
supports_pdfBOOLYesWhether this vendor sends invoices as PDF attachments.
supports_htmlBOOLYesWhether this vendor sends invoices as HTML email bodies.
logo_urlSNoPublic S3 URL to the vendor logo. Leave as "" if not yet uploaded.
created_atSYesISO 8601 creation timestamp.
updated_atSYesISO 8601 last-update timestamp.

The upload_vendors.sh script

#!/bin/bash

# Script to upload vendor configurations to DynamoDB VendorConfig table
# Usage: ./upload_vendors.sh

# Color codes for output
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

TABLE_NAME="VendorConfig"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

echo "========================================="
echo "Uploading vendor configs to DynamoDB"
echo "Table: $TABLE_NAME"
echo "========================================="
echo ""

# Counter for statistics
total=0
success=0
already_exists=0
failed=0

# Iterate over all JSON files in subdirectories
for config_file in "$SCRIPT_DIR"/*/*.json; do
  # Check if any JSON files exist
  if [ ! -f "$config_file" ]; then
    echo -e "${YELLOW}No JSON files found in $SCRIPT_DIR subdirectories${NC}"
    exit 1
  fi

  filename=$(basename "$config_file")
  vendor_id=$(echo "$filename" | sed 's/.json$//')

  total=$((total + 1))

  echo -n "Uploading $filename ... "

  # Run the put-item command with condition expression
  if aws dynamodb put-item \
    --table-name "$TABLE_NAME" \
    --item "file://$config_file" \
    --condition-expression "attribute_not_exists(vendor_id)" \
    --no-cli-pager \
    2>&1 | grep -q "ConditionalCheckFailedException"; then

    echo -e "${YELLOW}ALREADY EXISTS${NC}"
    already_exists=$((already_exists + 1))

  elif [ ${PIPESTATUS[0]} -eq 0 ]; then
    echo -e "${GREEN}SUCCESS${NC}"
    success=$((success + 1))
  else
    echo -e "${RED}FAILED${NC}"
    failed=$((failed + 1))
  fi
done

echo ""
echo "========================================="
echo "Upload Summary"
echo "========================================="
echo "Total files:      $total"
echo -e "${GREEN}Successful:       $success${NC}"
echo -e "${YELLOW}Already existed:  $already_exists${NC}"
echo -e "${RED}Failed:           $failed${NC}"
echo "========================================="

# Exit with error code if any failed
if [ $failed -gt 0 ]; then
  exit 1
fi
The script uses a conditional write (attribute_not_exists(vendor_id)) so running it multiple times is safe — existing records are never silently overwritten. If you need to update an existing vendor’s configuration, use aws dynamodb update-item or delete and re-upload the record.

Real vendor examples

These are the current vendor configs in the repository.
{
  "vendor_id": {"S": "dominos"},
  "vendor_name": {"S": "Dominos"},
  "invoice_category": {"S": "retail"},
  "invoice_sub_type": {"S": "food-delivery"},
  "default_email_patterns": {"SS": [
    "domino@dominos.se"
  ]},
  "default_subject_keywords": {"SS": [
    "Tack for din beställning"
  ]},
  "parser_type": {"S": "html"},
  "active": {"BOOL": true},
  "supports_pdf": {"BOOL": false},
  "supports_html": {"BOOL": true},
  "logo_url": {"S": "https://paypulse-vendor-logos.s3.eu-west-1.amazonaws.com/svgs/dominos.svg"},
  "created_at": {"S": "2025-10-06T10:30:00Z"},
  "updated_at": {"S": "2025-10-06T10:30:00Z"}
}
{
  "vendor_id": {"S": "foodora"},
  "vendor_name": {"S": "Foodora"},
  "invoice_category": {"S": "retail"},
  "invoice_sub_type": {"S": "food-delivery"},
  "default_email_patterns": {"SS": [
    "info@mail.foodora.se"
  ]},
  "default_subject_keywords": {"SS": [
    "Orderbekräftelse"
  ]},
  "parser_type": {"S": "html"},
  "active": {"BOOL": true},
  "supports_pdf": {"BOOL": false},
  "supports_html": {"BOOL": true},
  "logo_url": {"S": "https://paypulse-vendor-logos.s3.eu-west-1.amazonaws.com/pngs/foodora.png"},
  "created_at": {"S": "2025-10-06T10:30:00Z"},
  "updated_at": {"S": "2025-10-06T10:30:00Z"}
}
{
  "vendor_id": {"S": "zalando"},
  "vendor_name": {"S": "Zalando"},
  "invoice_category": {"S": "retail"},
  "invoice_sub_type": {"S": "clothing"},
  "default_email_patterns": {"SS": [
    "info@service-mail.zalando.se"
  ]},
  "default_subject_keywords": {"SS": [
    "Thanks for your order"
  ]},
  "parser_type": {"S": "html"},
  "active": {"BOOL": true},
  "supports_pdf": {"BOOL": false},
  "supports_html": {"BOOL": true},
  "logo_url": {"S": "https://paypulse-vendor-logos.s3.eu-west-1.amazonaws.com/svgs/zalando.svg"},
  "created_at": {"S": "2025-10-06T10:30:00Z"},
  "updated_at": {"S": "2025-10-06T10:30:00Z"}
}
{
  "vendor_id": {"S": "jack&jones"},
  "vendor_name": {"S": "Jack & Jones"},
  "invoice_category": {"S": "retail"},
  "invoice_sub_type": {"S": "clothing"},
  "default_email_patterns": {"SS": [
    "noreply@jackjones.com"
  ]},
  "default_subject_keywords": {"SS": [
    "Orderbekräftelse"
  ]},
  "parser_type": {"S": "html"},
  "active": {"BOOL": true},
  "supports_pdf": {"BOOL": false},
  "supports_html": {"BOOL": true},
  "logo_url": {"S": ""},
  "created_at": {"S": "2025-10-18T10:30:00Z"},
  "updated_at": {"S": "2025-10-18T10:30:00Z"}
}
{
  "vendor_id": {"S": "anthropic"},
  "vendor_name": {"S": "Anthropic"},
  "invoice_category": {"S": "retail"},
  "invoice_sub_type": {"S": "subscriptions"},
  "default_email_patterns": {"SS": [
    "invoice+statements@mail.anthropic.com"
  ]},
  "default_subject_keywords": {"SS": [
    "Your receipt from Anthropic"
  ]},
  "parser_type": {"S": "html"},
  "active": {"BOOL": true},
  "supports_pdf": {"BOOL": false},
  "supports_html": {"BOOL": true},
  "logo_url": {"S": ""},
  "created_at": {"S": "2026-02-17T10:30:00Z"},
  "updated_at": {"S": "2026-02-17T10:30:00Z"}
}
{
  "vendor_id": {"S": "mevlana"},
  "vendor_name": {"S": "Mevlana Moské"},
  "invoice_category": {"S": "retail"},
  "invoice_sub_type": {"S": "subscriptions"},
  "default_email_patterns": {"SS": [
    "invoice+statements@mevlanagoteborg.se"
  ]},
  "default_subject_keywords": {"SS": [
    "Ditt kvitto från Mevlana Moské Göteborg"
  ]},
  "parser_type": {"S": "html"},
  "active": {"BOOL": true},
  "supports_pdf": {"BOOL": false},
  "supports_html": {"BOOL": true},
  "logo_url": {"S": ""},
  "created_at": {"S": "2026-02-08T10:30:00Z"},
  "updated_at": {"S": "2026-02-08T10:30:00Z"}
}

Notes and warnings

Set active to false to disable a vendor without deleting its record. The get_active_vendors() function filters on active = true, so the vendor will be excluded from all fetch runs until you re-enable it.
default_email_patterns and default_subject_keywords must be specific. A broad sender address like noreply@gmail.com or a generic subject keyword like Order will match promotional and transactional emails that are not invoices, causing the parser to process irrelevant content and producing noise in the invoice store.