Frappe_Claude_Skill_Package frappe-impl-integrations
git clone https://github.com/OpenAEC-Foundation/Frappe_Claude_Skill_Package
T=$(mktemp -d) && git clone --depth=1 https://github.com/OpenAEC-Foundation/Frappe_Claude_Skill_Package "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/source/impl/frappe-impl-integrations" ~/.claude/skills/openaec-foundation-frappe-claude-skill-package-frappe-impl-integrations && rm -rf "$T"
skills/source/impl/frappe-impl-integrations/SKILL.mdFrappe Integrations
Step-by-step workflows for OAuth, Webhooks, Payment Gateways, Data Import/Export, and external API calls.
Version: v14/v15/v16
Decision Tree: Which Integration Pattern?
WHAT ARE YOU INTEGRATING? │ ├─► External service needs to call YOUR Frappe site? │ ├─► On document events → Webhook (push to external) │ ├─► External sends data to you → Whitelisted API endpoint │ └─► External needs user auth → OAuth 2.0 Provider │ ├─► YOUR Frappe site calls an external service? │ ├─► Needs user-level OAuth consent → Connected App │ ├─► Server-to-server with API key → make_request / requests │ └─► Recurring sync → Scheduler + API calls │ ├─► Bulk data in/out? │ ├─► Import CSV/XLSX → Data Import DocType │ ├─► Export data → Report Builder / export-csv / API │ └─► Programmatic bulk → frappe.get_doc().insert() │ ├─► Payment processing? │ └─► Payment Request + Payment Gateway controller │ └─► Real-time vs batch? ├─► Real-time → Webhook or API endpoint ├─► Near real-time → frappe.enqueue() after event └─► Batch → Scheduler task (hourly/daily)
Workflow 1: OAuth 2.0: Frappe as Provider
Use when external applications need "Sign in with Frappe" or API access on behalf of users.
Step 1: Configure OAuth Provider Settings
Navigate to Setup > Integrations > OAuth Provider Settings:
- Force: ALWAYS asks user for confirmation
- Auto: Asks only if no active token exists
Step 2: Create OAuth Client
Navigate to Setup > Integrations > OAuth Client:
| Field | Value |
|---|---|
| App Name | External app identifier |
| Scopes | Space-separated (e.g., ) |
| Redirect URIs | Space-separated callback URLs |
| Default Redirect URI | Primary callback URL |
| Grant Type | (RECOMMENDED) or |
| Response Type | (for Auth Code) or (for Implicit) |
| Skip Authorization | Check for trusted first-party apps only |
Step 3: Use the Generated Endpoints
| Endpoint | URL |
|---|---|
| Authorize | |
| Token | |
| Profile | |
Step 4: Configure External App
# Example: Grafana generic_oauth config client_id = <generated_client_id> client_secret = <generated_client_secret> auth_url = https://your-frappe.com/api/method/frappe.integrations.oauth2.authorize token_url = https://your-frappe.com/api/method/frappe.integrations.oauth2.get_token api_url = https://your-frappe.com/api/method/frappe.integrations.oauth2.openid_profile scopes = openid all
Critical Rules
- NEVER use
grant type for server-side apps — useImplicitAuthorization Code - ALWAYS use HTTPS in production for all OAuth endpoints
- NEVER expose
in client-side JavaScriptclient_secret
Workflow 2: Connected App: Frappe as OAuth Consumer
Use when your Frappe instance needs to access external services (Google, Microsoft, etc.) on behalf of users.
Step 1: Create Connected App DocType
| Field | Purpose |
|---|---|
| Name | Identifier for the connection |
| OpenID Configuration URL | Auto-fetches endpoints (e.g., ) |
| Authorization URI | Consent screen URL (auto-filled from OpenID) |
| Token URI | Token exchange URL (auto-filled from OpenID) |
| Redirect URI | Auto-generated — copy this to external provider |
| Client ID | From external provider |
| Client Secret | From external provider |
| Scopes | Permissions needed (e.g., ) |
Step 2: Register Redirect URI with Provider
Copy the auto-generated Redirect URI and register it in the external provider's OAuth console.
Step 3: Add Extra Parameters (if needed)
access_type=offline # Google: enables refresh tokens prompt=consent # Google: forces re-consent for refresh token
Step 4: Use in Code
import frappe connected_app = frappe.get_doc("Connected App", "My Google App") # Initiates OAuth flow — user clicks "Connect to..." button # After consent, tokens are stored automatically # Making authenticated calls: session = connected_app.get_oauth2_session() response = session.get("https://www.googleapis.com/gmail/v1/users/me/messages")
Critical Rules
- ALWAYS add
for Google APIs to get refresh tokensaccess_type=offline - NEVER store tokens manually — Connected App manages token lifecycle
- ALWAYS handle
— callTokenExpiredError
or reconnectsession.refresh_token()
Workflow 3: Webhooks: Push Notifications to External Services
Step 1: Create Webhook DocType
Navigate to Integrations > Webhook:
| Field | Value |
|---|---|
| DocType | Target document type |
| Doc Event | , , , , |
| Request URL | External endpoint |
| Request Method | POST (default) |
| Conditions | Optional Jinja filter (e.g., ) |
| Enabled | Check to activate |
Step 2: Configure Headers
Add custom headers for authentication:
Authorization: Bearer <api_token> Content-Type: application/json
Step 3: Configure Data: Choose Format
Form URL-encoded: Select specific fields from a table.
JSON: Use Jinja templates for structured payloads:
{ "id": "{{ doc.name }}", "total": "{{ doc.grand_total }}", "items": {{ doc.items | tojson }}, "event": "{{ event }}" }
Step 4: Enable Webhook Secret (HMAC Verification)
Set a Webhook Secret — Frappe adds
X-Frappe-Webhook-Signature header with base64-encoded HMAC-SHA256 hash of the payload.
Receiver verification (Python example):
import hmac, hashlib, base64 def verify_webhook(payload_body, secret, signature_header): expected = base64.b64encode( hmac.new(secret.encode(), payload_body, hashlib.sha256).digest() ).decode() return hmac.compare_digest(expected, signature_header)
Critical Rules
- ALWAYS enable Webhook Secret for production webhooks
- NEVER rely on webhooks for guaranteed delivery — implement idempotency on the receiver
- ALWAYS use
filter for child table data in JSON payloads| tojson - Webhook logs are created for every delivery — check Webhook Request Log for debugging
Workflow 4: External API Calls from Frappe
Using frappe.integrations.utils
from frappe.integrations.utils import make_get_request, make_post_request # GET request response = make_get_request( "https://api.example.com/data", headers={"Authorization": "Bearer token123"} ) # POST request response = make_post_request( "https://api.example.com/submit", data={"key": "value"}, headers={"Content-Type": "application/json"} )
Using requests Library Directly
import requests import frappe def sync_to_external(): try: response = requests.post( "https://api.example.com/endpoint", json={"data": "value"}, timeout=30 ) response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: frappe.log_error(f"API call failed: {e}", "Integration Error") raise
Critical Rules
- ALWAYS set a
on external requests (30s recommended)timeout - ALWAYS wrap external calls in try/except and log errors with
frappe.log_error() - NEVER call external APIs inside
orvalidate
— usebefore_save
+on_updatefrappe.enqueue() - ALWAYS use
for slow external calls to avoid blocking the web requestfrappe.enqueue()
Workflow 5: Data Import
Via UI (Data Import DocType)
- Navigate to Home > Data Import > New
- Select DocType and Import Type (
orInsert
)Update - Download template CSV/XLSX
- Fill in data following the template format
- Upload and preview
- Start Import
CSV Format Rules
ID,Item Name,Item Group,Stock UOM ,Widget A,Products,Nos ,Widget B,Raw Material,Kg
- First row: field labels or API field names
- Leave
/ID
empty for Insert (auto-generated)name - For Update:
column MUST contain existing document namesID - Child tables: repeat parent row data, add child fields as extra columns
Programmatic Import
import frappe from frappe.core.doctype.data_import.data_import import DataImport # Create Data Import document di = frappe.get_doc({ "doctype": "Data Import", "reference_doctype": "Item", "import_type": "Insert New Records", "import_file": "/path/to/file.csv" }) di.insert() di.start_import()
Critical Rules
- ALWAYS download and use the template — column order and names must match exactly
- NEVER import more than 5,000 rows at once — split into batches
- ALWAYS test with 5-10 rows first before bulk import
- ALWAYS check Import Log for row-level errors after import completes
Workflow 6: Data Export
Via Report Builder
- Open any DocType list view
- Apply filters
- Menu > Export (CSV/Excel)
Via CLI
bench --site mysite export-csv "Sales Invoice" bench --site mysite export-doc "Sales Invoice" "INV-001" bench --site mysite export-json "Sales Invoice" "INV-001" bench --site mysite export-fixtures --app myapp
Programmatic Export
import frappe # Export filtered data data = frappe.get_all("Sales Invoice", filters={"status": "Paid", "posting_date": [">", "2024-01-01"]}, fields=["name", "customer", "grand_total", "posting_date"], order_by="posting_date desc", limit_page_length=0 # No limit ) # Convert to CSV import csv, io output = io.StringIO() writer = csv.DictWriter(output, fieldnames=["name", "customer", "grand_total", "posting_date"]) writer.writeheader() writer.writerows(data) csv_content = output.getvalue()
Workflow 7: Frappe REST API Authentication
API Key + Secret (Server-to-Server)
# Generate via User > API Access > Generate Keys curl -H "Authorization: token api_key:api_secret" \ https://your-site.com/api/resource/Sales%20Invoice
OAuth Bearer Token
curl -H "Authorization: Bearer access_token" \ https://your-site.com/api/resource/Sales%20Invoice
Session-Based (Login)
# Login first curl -X POST https://your-site.com/api/method/login \ -d "usr=user@example.com&pwd=password" # Subsequent requests use session cookie
Integration Patterns: Sync vs Async
| Pattern | When to Use | Implementation |
|---|---|---|
| Synchronous | Response needed immediately | Direct API call in controller |
| Async (enqueue) | External call > 5s | |
| Webhook | Push on event | Webhook DocType configuration |
| Scheduled sync | Periodic batch | in hooks.py |
| Real-time | Live updates | Socket.IO + |
Retry Pattern
import frappe from frappe.utils.background_jobs import get_jobs def sync_with_retry(doc_name, retry_count=0, max_retries=3): try: result = call_external_api(doc_name) frappe.db.set_value("Sales Invoice", doc_name, "sync_status", "Success") frappe.db.commit() except Exception as e: if retry_count < max_retries: frappe.enqueue( "myapp.integrations.sync_with_retry", doc_name=doc_name, retry_count=retry_count + 1, queue="short", enqueue_after_commit=True ) else: frappe.log_error(f"Sync failed after {max_retries} retries: {e}") frappe.db.set_value("Sales Invoice", doc_name, "sync_status", "Failed") frappe.db.commit()
Version Differences
| Feature | V14 | V15 | V16 |
|---|---|---|---|
| Webhook DocType | Yes | Yes | Yes |
| Connected App | Yes | Yes | Yes |
| OAuth 2.0 Provider | Yes | Yes | Yes |
| Data Import (new UI) | Yes | Yes | Yes |
| Print Designer | No | Yes | Yes |
| Yes | Yes | Yes |
| Webhook HMAC | Yes | Yes | Yes |
Reference Files
| File | Contents |
|---|---|
| workflows.md | Complete integration workflow patterns |
| examples.md | Working code examples for all integration types |
| anti-patterns.md | Common integration mistakes and fixes |
| decision-tree.md | Extended decision trees for integration choice |