用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/D1DX/morning-skill --skill morning命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
基于 SOC 职业分类
正在显示 SKILL.md
| name | morning |
| description | Morning (Green Invoice) full API reference — expenses, file upload, classifications. |
| disable-model-invocation | false |
| user-invokable | true |
| argument-hint | task description |
Full reference for the Morning/Green Invoice API. Sections 1–9 cover expenses, file upload, and classifications via raw API (Python). Sections 10–18 cover all remaining endpoints via morning-cli.
Sections 1–9: Source: Apiary spec at https://jsapi.apiary.io/apis/greeninvoice.json.
Sections 10–18: Verified against live sandbox.
Sections 1–9 use the raw API directly (Python/curl). For morning-cli auth (sections 10–18), see Section 10.
import requests, json
# Token fetch is the OAuth2 client-credentials call on the IDP host (api.morning.co).
# The legacy POST https://api.greeninvoice.co.il/api/v1/account/token
# ({id, secret} -> {token}) is BLOCKED. The id/secret VALUES are
# unchanged — only the endpoint, field names, and grant_type change.
resp = requests.post("https://api.morning.co/idp/v1/oauth/token",
json={'grant_type': 'client_credentials',
'client_id': API_KEY, 'client_secret': API_SECRET})
token = resp.json()['accessToken']
headers = {'Authorization': f'Bearer {token}'}
The response is {accessToken, tokenType: "Bearer", expiresAt} — expiresAt is a
Unix timestamp (~1 hour out); re-authenticate once it passes. Only the token FETCH
moves host; every standard API call below stays on api.greeninvoice.co.il/api/v1.
Base URLs:
https://api.morning.co — path /idp/v1/oauth/token (sandbox https://api.sandbox.morning.dev)https://api.greeninvoice.co.il/api/v1https://sandbox.d.greeninvoice.co.il/api/v1https://apigw.greeninvoice.co.il/file-upload/v1/url| Method | Endpoint | Purpose |
|---|---|---|
POST | /expenses | Create expense |
GET | /expenses/{id} | Get expense |
PUT | /expenses/{id} | Update expense |
DELETE | /expenses/{id} | Delete expense |
POST | /expenses/search | Search expenses |
POST | /expenses/{id}/open | Reopen reported expense |
POST | /expenses/{id}/close | Report/close expense |
GET | /expenses/statuses | Allowed statuses |
POST | /expenses/drafts/search | Search expense drafts |
GET | /accounting/classifications/search | List classifications (POST with {} body) |
GET | /accounting/classifications/map | Classification map |
PUT | /accounting/classifications/{id} | Update classification |
GET | apigw.../file-upload/v1/url | Get presigned S3 upload URL |
expense = {
'documentType': 305, # See document types below
'date': '2024-09-18', # Document date
'reportingDate': '2024-09-01', # First day of reporting month
'number': 'INV-001', # Document number / source ID
'currency': 'USD', # ILS, USD, EUR, THB, etc.
'amount': 100.00, # Total including VAT
'amountExcludeVat': 85.00, # Amount before VAT
'vat': 15.00, # VAT amount
'supplier': { # MUST be at root level, NOT inside data
'name': 'Vendor Name',
'taxId': '', # Israeli ח.פ. or empty for foreign
'country': 'US' # ISO country code
},
'accountingClassification': {
'id': 'd0c61a4a-...', # Morning classification UUID
'title': 'הוצ\' כלי עבודה',
'income': 100,
'vat': 100
},
'description': '', # Optional
: ,
: ,
}
resp = requests.post(, json=expense, headers=headers)
created = resp.json()
expense_id = created[]
Critical: supplier goes at the root level of the JSON, NOT inside a data object. Putting it in data.supplier causes error 3311.
| Value | Hebrew | Translation |
|---|---|---|
| 20 | חשבון / אישור תשלום | Invoice / Payment Confirmation |
| 305 | חשבונית מס | Tax Invoice |
| 320 | חשבונית מס / קבלה | Tax Invoice / Receipt |
| 330 | חשבונית זיכוי | Credit Invoice |
| 400 | קבלה | Receipt |
| 405 | קבלה על תרומה | Donation Receipt |
| Value | Hebrew | Translation |
|---|---|---|
| -1 | לא שולם | Unpaid |
| 1 | מזומן | Cash |
| 2 | צ'ק | Check |
| 3 | כרטיס אשראי | Credit Card |
| 4 | העברה בנקאית | Bank Transfer |
| 5 | פייפאל | PayPal |
| 10 | אפליקציית תשלום | Payment App |
| 11 | אחר | Other |
| Value | Hebrew | Translation |
|---|---|---|
| 10 | הוצאה פתוחה | Open (draft) |
| 20 | הוצאה מדווחת | Reported |
| 30 | סומנה ידנית כפתוחה | Manually reopened |
| 100 | הוצאה מחוקה | Deleted |
This is the critical flow for attaching PDFs to expenses. Morning does OCR on the uploaded file.
import requests, json, uuid
# Step 1: Create expense (no file yet)
expense = { ... } # See section 3
resp = requests.post(f'{base}/expenses', json=expense, headers=headers)
expense_id = resp.json()['id']
# Step 2: Get presigned S3 URL with expense ID
data_param = json.dumps({"source": 5, "id": expense_id, "state": "expense"})
resp = requests.get(
'https://apigw.greeninvoice.co.il/file-upload/v1/url',
params={'context': 'expense', 'data': data_param},
headers=headers
)
presigned = resp.json()
# Step 3: Download file (e.g. from Airtable)
pdf_data = requests.get(airtable_file_url).content
# Step 4: Upload to S3 — MUST use requests library for proper multipart
filename = f"expense_{uuid.uuid4().hex[:8]}.pdf"
upload_fields = {}
for key, value in presigned['fields'].items():
upload_fields[key] = (None, value)
upload_fields['file'] = (filename, pdf_data, 'application/pdf')
resp = requests.post(presigned['url'], files=upload_fields)
assert resp.status_code == 204 # S3 returns 204 No Content on success
# Step 5: Poll for file attachment (~5-15 seconds)
import time
for _ in range(6):
time.sleep(5)
resp = requests.get(f'{base}/expenses/{expense_id}', headers=headers)
resp.json().get(, {}).get():
data Parameter| Scenario | data value |
|---|---|
| New expense draft (no expense yet) | {"source": 5} |
| Attach file to existing expense | {"source": 5, "id": "<expense_id>", "state": "expense"} |
source: 5 is mandatory in all cases (means "API upload").
requests library for S3 multipart upload. Manual boundary construction with urllib does NOT work — the multipart encoding is subtly different and Morning's backend silently ignores the file.fields must be included in the multipart upload, appended BEFORE the file.file field must be last in the multipart form.# Get presigned URL WITHOUT expense ID
data_param = json.dumps({"source": 5})
resp = requests.get(
'https://apigw.greeninvoice.co.il/file-upload/v1/url',
params={'context': 'expense', 'data': data_param},
headers=headers
)
# Upload to S3 (same as above)
# Morning creates an expense DRAFT asynchronously
# Poll via POST /expenses/drafts/search
Note: Draft creation is fully async and may take longer. The draft must be manually approved to become an actual expense.
resp = requests.post(f'{base}/expenses/search', json={
'fromDate': '2024-01-01',
'toDate': '2024-12-31',
'page': 1,
'pageSize': 25,
# Optional filters:
'supplierName': 'Amazon',
'minAmount': 10,
'maxAmount': 500,
'reported': True,
'accountingClassificationId': '...',
}, headers=headers)
result = resp.json()
# result = {total, page, pageSize, pages, items: [...]}
Note: page and pageSize are required. Empty body {} works for fetching all.
resp = requests.post(f'{base}/accounting/classifications/search',
json={}, headers=headers)
for c in resp.json()['items']:
print(f"{c['id']}: {c['title']} (code={c['code']}, key={c['key']}, "
f"income={c['income']}, vat={c['vat']})")
Configure via Morning GUI only (Settings → Developer Tools).
| Event | Fires When |
|---|---|
expense-draft/parsed | Draft created from uploaded file |
expense-file/updated | Existing expense file replaced |
expense-draft/declined | Draft rejected |
file/infected | Uploaded file has virus |
| Code | Hebrew | Cause | Fix |
|---|---|---|---|
| 3306 | מספר מסמך הוצאה לא תקין | Missing number field | Add document number |
| 3310 | חודש דיווח לא תקין | Missing reportingDate | Add first-of-month date |
| 3311 | נא למלא פרטי ספק | Supplier missing or in wrong location | Move supplier to root level (not data.supplier) |
| 3312 | נא למלא פרטי סוג הוצאה | Missing accountingClassification | Add with valid id from classifications list |
| 1100 | שדה {id} לא תקין | Invalid UUID in path | Check expense ID is valid UUID |
For uploading many expenses from Airtable to Morning:
import requests, json, time, uuid
def upload_expense(record, token, base_url, classification_map):
"""Upload single record to Morning with file."""
headers = {'Authorization': f'Bearer {token}'}
# 1. Create expense
category = record.get('category', '')
classification = classification_map.get(category, {})
expense = {
'documentType': 305,
'date': record['date'],
'reportingDate': record['date'][:8] + '01',
'number': record.get('document_number', ''),
'currency': record['currency'],
'amount': record['amount'],
'amountExcludeVat': record['amount'], # Foreign = no VAT split
'vat': 0,
'supplier': {
'name': record.get('supplier_name', 'Unknown'),
'taxId': '',
'country': 'US' # Adjust per record
},
'accountingClassification': classification,
}
resp = requests.post(f'{base_url}/expenses', json=expense, headers=headers)
resp.raise_for_status()
expense_id = resp.json()['id']
# 2. Upload file
file_url = record[]
pdf_data = requests.get(file_url).content
data_param = json.dumps({: , : expense_id, : })
presigned = requests.get(
,
params={: , : data_param},
headers=headers
).json()
upload_fields = {k: (, v) k, v presigned[].items()}
upload_fields[] = (,
pdf_data, )
resp = requests.post(presigned[], files=upload_fields)
resp.status_code ==
_ ():
time.sleep()
exp = requests.get(,
headers=headers).json()
exp.get(, {}).get():
expense_id,
expense_id,
expiration field)Sections 11–18 document these command groups. All use morning-cli as the execution layer:
pip install morning-cli
morning-cli auth init --env sandbox # or production
All commands accept --env sandbox|production and --json for machine-readable output.
Use morning-cli <group> --help for the full subcommand list.
Do NOT use morning-cli for expense file uploads — use the two-step S3 presigned flow in Section 4.
morning-cli client)| Subcommand | What it does |
|---|---|
search | List/filter clients with pagination and text search |
get | Fetch a single client by UUID |
add | Create a new client |
update | Patch fields on an existing client |
balance | Get balance data for a client |
assoc | Associate a client with other entity IDs |
merge | Merge this client into a target client (destructive) |
delete | Delete a client by ID |
Search clients: the filter field is name — searchText/text are
silently ignored and return the full client list (verified against production).
morning-cli --env sandbox --json client search
morning-cli --env sandbox --json client search --data '{"name":"acme","pageSize":25,"page":1}'
Response shape:
{
"ok": true,
"op": "client.search",
"data": {
"pageSize": 25, "page": 1, "total": 1, "pages": 1,
"items": [
{
"id": "b6a1da5e-fdb6-4edd-87b8-e45b611d4370",
"name": "Test Client",
"active": true, "send": true,
"taxId": "", "accountingKey": "b5984494",
"city"
Get client:
morning-cli --env sandbox --json client get <CLIENT_ID>
Create client:
morning-cli --env sandbox --json client add --data '{"name":"Acme Ltd","country":"IL","emails":["billing@acme.com"]}'
Update client:
morning-cli --env sandbox --json client update <CLIENT_ID> --data '{"city":"Tel Aviv","remarks":"VIP"}'
Get balance:
morning-cli --env sandbox --json client balance <CLIENT_ID> --data '{}'
Returns {} when no transactions exist.
Associate:
morning-cli --env sandbox --json client assoc <CLIENT_ID> --data '{"ids":["<OTHER_CLIENT_ID>"]}'
Merge (destructive):
morning-cli --env sandbox --json client merge <SOURCE_ID> --data '{"id":"<TARGET_ID>"}'
Delete:
morning-cli --env sandbox --json client delete <CLIENT_ID> --yes
client balance requires --data '{}' — the flag is mandatory even with an empty bodyclient assoc requires ids in body — omitting returns error 2402send: true is set automatically on creation (clients receive documents); suppliers default send: false--file FILE accepted as alternative to --data on add/update/searchmorning-cli supplier)| Subcommand | What it does |
|---|---|
search | List/filter suppliers with pagination and text search |
get | Fetch a single supplier by UUID |
add | Create a new supplier |
update | Patch fields on an existing supplier |
merge | Merge this supplier into a target supplier (destructive) |
delete | Delete a supplier by ID |
Search suppliers:
morning-cli --env sandbox --json supplier search
morning-cli --env sandbox --json supplier search --data '{"searchText":"acme","pageSize":25,"page":1}'
Response shape mirrors clients — key differences: has businessId, fax, mobile, accountingClassificationId; lacks nameAliases, bankName/Branch/Account, self.
Create supplier:
morning-cli --env sandbox --json supplier add --data '{"name":"Acme Vendor","country":"IL","taxId":"000000000"}'
Update supplier:
morning-cli --env sandbox --json supplier update <SUPPLIER_ID> --data '{"city":"Tel Aviv"}'
Merge (destructive):
morning-cli --env sandbox --json supplier merge <SOURCE_ID> --data '{"id":"<TARGET_ID>"}'
Delete:
morning-cli --env sandbox --json supplier delete <SUPPLIER_ID> --yes
assoc or balance subcommands — those are client-onlyaccountingClassificationId links to an expense category — set this to auto-classify expenses from this suppliersend: false is the default for suppliersbyName.buckets keymorning-cli document)| Subcommand | What it does |
|---|---|
types | List all document type IDs and names (Hebrew) |
statuses | List all document status IDs and names |
templates | List visual templates |
info | Get defaults + next document number for a given type |
search | Search/list documents with filters |
payments-search | Search payment rows across documents |
get | Fetch a single document by ID |
linked | Fetch documents linked to a given document |
create | Create a new document |
preview | Preview without creating |
open | Reopen a closed document |
close | Close an open document |
download | Download document PDF |
Emailing / resending an existing document — raw API, no CLI subcommand. morning-cli has no send/distribute subcommand, and there is no
/sendor/notifyendpoint (both 404). To email an already-created document, POST to/documents/{id}/distributewith a bearer token:curl -s "https://api.greeninvoice.co.il/api/v1/documents/{id}/distribute" \ -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"attachment": false, "recipients": ["client@example.com"], "remarks": ""}' # 200 + {} = sent. attachment:true embeds the PDF; false sends the doc link. # recipients defaults best to the doc's own client emails (GET /documents/{id} → .client.emails).Morning otherwise emails ONLY at creation (
sendByEmail: truein the create payload). Re-creating a document to "resend" makes a DUPLICATE — use distribute.
| ID | English |
|---|---|
| 10 | Quote |
| 20 | Proforma / Payment confirmation |
| 300 | Transaction invoice |
| 305 | Tax invoice |
| 320 | Tax invoice + receipt (most common) |
| 330 | Credit invoice |
| 400 | Receipt |
| 500 | Purchase order |
| ID | Status |
|---|---|
| 0 | Open |
| 1 | Closed |
| 2 | Manually closed |
| 3 | Cancelling |
| 4 | Cancelled |
Get document types:
morning-cli --env sandbox --json document types
morning-cli --env sandbox --json document types --lang en
Get info/defaults for a type:
morning-cli --env sandbox --json document info --type 320
Returns: next document number, VAT rate, allowed VAT types, max backdating days, token (required by some create paths), currency/language defaults, feature flags.
Search documents:
morning-cli --env sandbox --json document search --data '{"type":320,"page":1}'
Useful filters: type, status, clientId, fromDate, toDate, page, pageSize.
Preview document (non-destructive):
morning-cli --env sandbox --json document preview --data '{
"type": 320,
"client": {"id": "<CLIENT_ID>"},
"currency": "ILS",
"vatType": 0,
"date": "2026-04-14",
"incomeRows": [{"description": "Service", "quantity": 1, "price": 1000, "currency": "ILS", "vatType": 0}],
"paymentRows": [{"date": "2026-04-14", "type": 1, "price": 1180, "currency": "ILS"}]
}'
Create document (same payload as preview — irreversible):
morning-cli --env sandbox --json document create --data '{ ... same as preview ... }'
Required fields: type, client.id, currency, vatType, date, at least one incomeRows entry with description + price.
paymentRows[].type)Note: Document payment types use the same numeric enum as the expense payment types in Section 3 — verified against the Morning API payment-type blueprint. Earlier revisions of this table had the values scrambled (e.g.
4shown as "Credit card");4is Bank transfer, confirmed by an issued document rendering "Wire transfer" for a type-4 row.
| ID | Type |
|---|---|
| -1 | Unpaid |
| 0 | Withholding tax (deduction at source) |
| 1 | Cash |
| 2 | Check |
| 3 | Credit card |
| 4 | Bank transfer |
| 5 | PayPal |
| 10 | Payment app |
| 11 | Other |
document info returns a token — some create paths require passing it to confirm the next number; fetch fresh immediately before createvatType: 0 = standard VAT included; vatType: 1 = zero VAT (exempt)maxDaysBack enforced by API — backdating beyond it will fail (~59 days in sandbox)signed: true in the payload finalizes (issues) the document; omit for draft if account supports it (unsignedEnabled in document info)morning-cli item)| Subcommand | What it does |
|---|---|
search | Search/list price list items |
get | Fetch a single item by UUID |
add | Create a new price list item |
update | Update an existing item |
delete | Delete an item |
List items:
morning-cli --env sandbox --json item search
morning-cli --env sandbox --json item search --data '{"page":1,"pageSize":25}'
Response shape:
{
"ok": true, "op": "item.search",
"data": {
"items": [
{
"id": "e0b445f5-9a02-4da7-bac1-c641a5d26d8c",
"name": "Test Item", "description": "",
"price": 200, "currency": "ILS",
"creationDate": 1776155844
}
]
}
}
Create item:
morning-cli --env sandbox --json item add --data '{"name":"Item Name","description":"Item description","price":100,"currency":"ILS"}'
Required fields: name, description, price, currency
Update item:
morning-cli --env sandbox --json item update <ITEM_ID> --data '{"name":"Updated","description":"Updated desc","price":150,"currency":"ILS"}'
Delete:
morning-cli --env sandbox --json item delete <ITEM_ID> --yes
update is a full replace — omitting any field clears it. Always send all fields you want to keep.morning-cli payment)| Subcommand | What it does |
|---|---|
tokens-search | Search saved payment tokens (stored cards) |
charge | Charge a saved payment token |
form | Create a hosted payment form |
Search tokens:
morning-cli --env sandbox --json payment tokens-search
Charge a token:
morning-cli --env sandbox --json payment charge <TOKEN_ID> --data '{"amount":100,"currency":"ILS"}'
Create payment form:
morning-cli --env sandbox --json payment form --data '{"documentType":<type>,"client":{...},"income":[...]}'
payment form returns error 2403 in sandbox — "Document type not supported for this business type." This is a sandbox account tier restriction, not a payload error.payment charge requires a real token UUID from tokens-search — passing any non-existent UUID returns error 1100tokens-search aggregations returns [] (empty array) not an object — shape differs from other search endpointsmorning-cli business)| Subcommand | What it does |
|---|---|
current | Active business profile |
list | All businesses under the account |
get | Specific business by UUID |
types | Legal entity types |
footer | Document footer configuration |
numbering-get | Document numbering sequences per type |
numbering-update | Update numbering sequences |
update | Update business profile |
file-upload | Upload logo/signature (--type 0=logo, 1=signature) |
file-delete | Delete uploaded file by type |
Get current business:
morning-cli --env sandbox --json business current
Response includes: id, type, taxId, name, address, cityId, category, subCategory, exemption, inboundEmail, settings (currency, lang, vatType, taxAuthorityConnected).
Get numbering sequences:
morning-cli --env sandbox --json business numbering-get
Returns one entry per document type: type, number, nextNumber, lastDocumentDate, active.
Get business types:
morning-cli --env sandbox --json business types --lang en
| ID | English |
|---|---|
| 1 | Licensed dealer |
| 2 | Ltd. company |
| 3 | Exempt dealer |
| 4 | Non-profit / NGO |
| 5 | Public benefit company |
| 6 | Partnership |
business current and business list return identical data — current is shorthand for the single credentialed businessbusiness footer returns {} when no footer is configured — not an errorbusiness get requires the UUID, not the taxIdmorning-cli partner)| Subcommand | What it does |
|---|---|
users | List all users connected to the partner account |
get | Get a specific user by email |
connect | Connect a business to the partner account |
disconnect | Disconnect a business |
morning-cli tools)| Subcommand | What it does |
|---|---|
currencies | All supported currencies with live ILS exchange rates |
countries | All countries with alpha2/alpha3 codes and calling codes |
cities | Cities for a given country (--country required, ISO alpha2) |
occupations | Full occupation/category tree for business classification |
Get currencies (live rates):
morning-cli --env sandbox --json tools currencies
Returns 31 currencies vs ILS base. Each has spot.current (real-time) and closing.current (previous business day official rate).
Supported: ILS, AED, AUD, BGN, BRL, CAD, CHF, CNY, CZK, DKK, EUR, GBP, HKD, HUF, IDR, INR, JPY, KRW, MXN, NOK, NZD, PHP, PLN, RON, RUB, SEK, SGD, THB, TRY, USD, ZAR
Get countries:
morning-cli --env sandbox --json tools countries --locale en
Get cities:
morning-cli --env sandbox --json tools cities --country IL
City id maps to business.cityId.
Get occupations tree:
morning-cli --env sandbox --json tools occupations --locale en
Two-level tree: parent category + child subCategory. IDs map to business.category / business.subCategory.
tools cities --country requires ISO alpha2 (IL, US) — not alpha3--locale he (Hebrew). Pass --locale en for English.currencies returns live rates — not static. Spot and closing rates differ.