# 海关申报 API v1.0 ## Description This is version `1.0.0` of this API documentation. Last update on Dec 5, 2025. 海关申报 API 提供申报管理功能,包括创建、确认和取消申报。 **重要说明**:申报一旦提交到海关系统就不能修改,如需修改请重新创建申报。 ## 认证 参考 [Bearer token](https://learning.postman.com/docs/sending-requests/authorization/#bearer-token) 认证方式 ## 幂等请求 API 支持幂等请求,您可以安全的重试请求,而不会意外的执行两次相同操作,要执行一个幂等请求须在请求头中增加: `Idempotency-Key: ` 重试时保证 key 不变 幂等请求仅支持使用 **POST** 方式的请求,其他方式的请求将被忽略 ## 分页 分页按照 `rfc5988` 标准 ```python import requests response = requests.get('https://app-sandbox.gutline.eu/api/v1/declarations', headers={'Authorization': 'Bearer token'}) next_page_url = response.links['next']['url'] response = requests.get(next_page_url, headers={'Authorization': 'Bearer token'}) ``` ## 请求限制 * `X-RateLimit-Limit` 每小时请求次数限制 * `X-RateLimit-Remaining` 剩余请求次数 * `X-RateLimit-Reset` 下次重置时间 UTC ## Webhook 通知 当申报状态发生变化时,我们会向您配置的 webhook 端点发送 HTTP POST 请求。 ### 事件类型 | 事件类型 | 触发时机 | 描述 | |---------|---------|------| | `declaration.updated` | 申报状态更新 | 当申报状态发生变化时触发 | ### 请求格式 #### HTTP Headers ```http Content-Type: application/json X-Webhook-Event: declaration.updated X-Webhook-Signature: sha256=abc123... X-Webhook-Delivery: jid-123456 User-Agent: Gutline-Hookshot/0.1 ``` 注意:签名格式为 `算法=签名值`,如 `sha256=abc123...`,其中 `sha256` 是算法,`abc123...` 是签名值。 #### 请求体结构 ```json { "event": "declaration.updated", "payload": { "object": "declaration", "id": 222537, "reference": "DECL_20251020_021323_65558849", "template_code": "NLAMS_H7", "client_ref": "NLH7-1727866234", "lrn": "NLH7-1727866234", "mrn": "25NLNG746OHK9FBER0", "declaration_type": "IM", "additional_declaration_type": "A", "declaration_office_code": "NL000567", "presentation_office_code": null, "shipment_carrier_ref": null, "mode_at_border": "air", "inland_mode": null, "country_of_dispatch_code": "CN", "country_of_destination_code": "NL", "gross_weight_in_kg": "0.5", "total_packages": 10, "unique_consignment_reference": null, "location_of_goods_code": null, "location_of_goods_type": null, "delivery_terms": null, "delivery_terms_location": null, "currency_code": "EUR", "currency_exchange_rate": null, "ioss_number": "IM9000000001", "deferred_payment_account": null, "status": "released", "reject_reason": null, "provisional_validation_datetime": "2025-10-01 12:00:00", "total_intrinsic_value": "24.0", "total_freight_cost": "0.0", "total_insurance_cost": "0.0", "total_invoice_value": "24.0", "cancellation_requested_at": null, "cancelled_at": null, "submitted_at": null, "rejected_at": null, "accepted_at": "2025-10-20T02:13:47.278Z", "inspected_at": null, "released_at": "2025-10-20T02:13:47.325Z", "archived_at": null, "goods_presentation_effective_date": "2025-10-20", "goods_presentation_due_date": "2025-11-19", "importer": { "eori_number": "DE123456789", "name": "Import Company B.V.", "attention_to": "Hans Michaël", "street_address": "Beëk Street 1", "city": "Rotterdam", "postal_code": "3000 AA", "state": null, "country_code": "NL", "email": "hans@test.com", "phone": "+49 30 123456", "vat_number": null }, "exporter": { "eori_number": null, "name": "Shanghai Exports Ltd", "attention_to": null, "street_address": "Seller Street 1", "city": "Shanghai", "postal_code": "200000", "state": null, "country_code": "CN", "email": null, "phone": null }, "items": [ { "id": 221436, "description": "T-shirts", "commodity_code": "6109100010", "national_additional_codes": [], "requested_procedure": null, "additional_procedure_codes": [ "F48", "C07" ], "currency_code": "EUR", "gross_weight_in_grams": 500, "net_weight_in_grams": 450, "package_type": null, "number_of_packages": 1, "shipping_marks": null, "quantity": 2, "supplementary_units": null, "supplementary_units_amount": null, "country_of_origin": "CN", "dangerous_goods_code": null, "ecommerce_ref": null, "ecommerce_url": null, "created_at": "2025-10-20T02:13:23.425Z", "updated_at": "2025-10-20T02:13:23.425Z", "intrinsic_value": "24.0", "freight_cost": null, "insurance_cost": null, "customs_value": "24.0", "documents": [], "tax_lines": [ { "id": 97, "type_code": "B00", "category": "vat", "regime_code": null, "rate": "0.0", "currency": "EUR", "base_amount": "24.0", "deduct_amount": "0.0", "amount": "0.0" } ] } ], "documents": [ { "id": 478104, "category": "supporting", "type_code": "N380", "reference_number": "INV-SH-2025-001", "valid_from": "2025-10-01T00:00:00.000Z", "file_url": null, "type_description": "Commercial invoice" }, { "id": 478105, "category": "transport", "type_code": "N740", "reference_number": "WBL12345678", "valid_from": "2025-10-01T00:00:00.000Z", "file_url": null, "type_description": "Air Waybill" } ], "tax_lines": [ { "id": 98, "type_code": "B00", "category": "vat", "regime_code": null, "rate": "0.0", "currency": "EUR", "base_amount": "24.0", "deduct_amount": "0.0", "amount": "0.0" } ], "created_at": "2025-10-20T02:13:23.408Z", "updated_at": "2025-10-20T02:13:47.354Z", "url": "https://app-sandbox.gutline.eu/api/v1/declarations/NLH7-1727866234" } } ``` ### 安全验证 我们使用 HMAC 算法对 webhook payload 进行签名验证,支持多种哈希算法(如 SHA256)。 **设计理念**:使用 `算法=签名值` 的格式,使得我们可以在后期升级到更安全的算法时,所有客户端都能自动适配,无需修改代码。这确保了系统的向前兼容性和安全性。 ```ruby # Ruby 示例 def verify_webhook_signature(payload, signature, secret) algorithm, signature_value = signature.split('=', 2) expected_signature = OpenSSL::HMAC.hexdigest(algorithm, secret, payload) Rack::Utils.secure_compare(signature_value, expected_signature) end # 使用示例 - 注意 payload 是 raw body def handle_webhook payload = request.body.read # 获取 raw body signature = request.headers['X-Webhook-Signature'] secret = ENV['WEBHOOK_SECRET'] unless verify_webhook_signature(payload, signature, secret) return head :unauthorized end # 解析 JSON event_data = JSON.parse(payload) # 处理事件 begin process_webhook_event(event_data) head :ok # 处理成功返回 200 rescue => e Rails.logger.error "Webhook processing failed: #{e.message}" head :internal_server_error # 处理失败返回 500,触发重试 end end ``` ```python # Python 示例 import hmac import hashlib import json def verify_webhook_signature(payload, signature, secret): algorithm, signature_value = signature.split('=', 1) expected_signature = hmac.new( secret.encode(), payload.encode(), getattr(hashlib, algorithm) ).hexdigest() return hmac.compare_digest(signature_value, expected_signature) # 使用示例 - 注意 payload 是 raw body @app.route('/webhooks', methods=['POST']) def handle_webhook(): payload = request.get_data() # 获取 raw body signature = request.headers.get('X-Webhook-Signature') secret = os.environ.get('WEBHOOK_SECRET') if not verify_webhook_signature(payload, signature, secret): return 'Unauthorized', 401 try: # 解析 JSON event_data = json.loads(payload) # 处理事件 process_webhook_event(event_data) return 'OK', 200 # 处理成功返回 200 except Exception as e: print(f"Webhook processing failed: {e}") return 'Internal Server Error', 500 # 处理失败返回 500,触发重试 ``` ```javascript // Node.js 示例 const crypto = require('crypto'); function verifyWebhookSignature(payload, signature, secret) { const [algorithm, signatureValue] = signature.split('='); const expectedSignature = crypto .createHmac(algorithm, secret) .update(payload) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(signatureValue), Buffer.from(expectedSignature) ); } // 使用示例 - 注意 payload 是 raw body app.post('/webhooks', (req, res) => { const payload = JSON.stringify(req.body); // 获取 raw body const signature = req.headers['x-webhook-signature']; const secret = process.env.WEBHOOK_SECRET; if (!verifyWebhookSignature(payload, signature, secret)) { return res.status(401).send('Unauthorized'); } try { // 解析 JSON const eventData = JSON.parse(payload); // 处理事件 processWebhookEvent(eventData); res.status(200).send('OK'); // 处理成功返回 200 } catch (error) { console.error('Webhook processing failed:', error); res.status(500).send('Internal Server Error'); // 处理失败返回 500,触发重试 } }); ``` ### 重试机制 - **最多重试 5 次**:如果您的端点返回非 2xx 状态码,我们会重试 - **重试间隔**:120 秒 - **失败处理**:连续失败 50 次后,webhook 会被暂停 - **自动禁用**:暂停 3 天后,webhook 会被自动禁用 - **建议**:为了确保事件不丢失,建议在事件处理完全成功后再返回 200 状态码 - **数据更新**:每次重试都会发送最新的数据,状态可能已经发生变化 - **设计理念**:避免存储大量过期数据,过期数据没有意义,建议使用时间戳判断状态变化 ### 响应要求 - **状态码**:必须返回 2xx 状态码表示成功 - **响应时间**:建议在 30 秒内响应 - **响应格式**:建议返回 JSON 格式响应 - **重要提醒**:建议确保事件处理完全成功后再返回 200 状态码,以避免事件丢失 ### 事件状态说明 #### 申报状态流转 ``` pending → submitted → acknowledged → accepted → released ``` **状态流转图**: ``` 正常申报流程: pending ──→ submitted ──→ accepted ──→ released │ ▼ rejected 预申报流程: pending ──→ submitted ──→ acknowledged ──→ accepted ──→ released │ ▼ rejected ``` **说明**: - 申报从 `pending` 状态开始 - `acknowledged` 状态仅用于预申报 - 正常申报流程:`pending → submitted → accepted → released` - 预申报流程:`pending → submitted → acknowledged → accepted → released` - 任何状态都可能被 `rejected` 或 `cancelled` #### 异常状态 ``` submitted → rejected (海关拒绝) submitted → cancelled (取消) ``` #### 重要提醒 **重试时数据会更新**:每次重试都会发送最新的数据,状态可能已经发生变化。例如: - 第一次通知:`status: "accepted"` - 重试通知:`status: "released"` **建议使用时间戳判断**:不要根据状态字段判断,而是根据对应的时间戳字段(如 `accepted_at`, `released_at`)来判断是否发生了新的状态变化。这些时间戳字段只会设置一次,不会更新,能准确判断是否发生了新的状态变化。这种设计避免了存储大量过期数据,过期数据也没有意义。 ### 常见问题 **Q: 重试时数据会变化吗?** A: 是的,每次重试都会发送最新的数据。如果第一次通知时状态是 `accepted`,重试时可能已经是 `released`。这是正常的设计,确保接收方总是获得最新状态。 **Q: 如何处理状态变化?** A: 建议使用关键时间戳字段来判断是否发生了新的状态变化。主要关注以下字段: **关键时间戳字段**: - `submitted_at` - 提交时间 - `accepted_at` - 接受时间 - `inspected_at` - 查验时间 - `released_at` - 放行时间 - `cancelled_at` - 取消时间 - `rejected_at` - 拒绝时间 **关键业务字段**: - `mrn` - 海关参考号 - `reject_reason` - 拒绝原因 **处理示例**: ```ruby # 检查是否被接受 if payload['accepted_at'] && !last_processed_accepted_at # 处理接受逻辑 last_processed_accepted_at = payload['accepted_at'] end # 检查是否被放行 if payload['released_at'] && !last_processed_released_at # 处理放行逻辑 last_processed_released_at = payload['released_at'] end ``` **Q: 为什么使用时间戳而不是状态?** A: 这种设计有两个重要优势: 1. **避免存储压力**:不需要存储大量过期数据 2. **过期数据无意义**:过期数据对业务没有价值 3. **精确判断**:时间戳字段(如 `accepted_at`、`released_at`)只会设置一次,能准确判断是否发生了新的状态变化 **Q: 如何处理大量事件?** A: 建议异步处理事件,快速返回 2xx 状态码。 **Q: 如何获取 webhook 密钥?** A: 在创建 webhook 时,系统会自动生成密钥,请妥善保存。 **Q: 如何测试 webhook 配置?** A: 使用 Dashboard 中的 ping 功能发送测试请求。 **Q: 为什么使用 `算法=签名值` 的格式?** A: 这种格式允许我们在后端升级到更安全的算法时,所有客户端都能自动适配,无需修改代码。这确保了系统的向前兼容性和安全性。 **Q: 支持哪些签名算法?** A: 我们目前使用 SHA256 算法生成签名。随着时间推移和算力提升,我们会主动升级到更安全的算法(如 SHA384、SHA512 等)。您的代码应支持动态解析签名头中的算法名称,以便在我们升级算法时无需修改代码即可兼容。 **Q: 为什么建议处理完成后才返回 200?** A: 返回 200 状态码表示处理成功,我们不会重试该事件。为了确保事件不丢失,建议在业务逻辑完全处理成功后再返回 200 状态码。 **Q: payload 是原始字符串还是解析后的对象?** A: payload 是原始的 JSON 字符串(raw body),建议先解析为对象再使用。签名验证需要使用原始字符串。 ## 荷兰海关申报指南 | 实体 | 字段 | 类型 | DECO (F48) | DECO (F49) | DMS | 描述 | | --- | --- | --- | --- | --- | --- | --- | | Declaration | declaration_type | string | ✓ | ✓ | ✓ | IM | | Declaration | additional_declaration_type | string | D | D | A | A: 标准申报 D: 预申报 | | Declaration | declaration_office_code | string | ✓ | ✓ | ✓ | NL000567 | | Declaration | country_of_dispatch_code | string | ✓ | ✓ | ✓ | CN | | Declaration | country_of_destination_code | string | ✓ | ✓ | ✓ | NL | | Declaration | mode_at_border | string | x | x | ✓ | sea,rail,road,air | | Declaration | ioss_number | string | ✓ | x | x | IM9000000001 | | Declaration | location_of_goods_country_code | string | ✓ | ✓ | ✓ | NL | | Declaration | location_of_goods_postal_code | string | ✓ | ✓ | ✓ | 1118 CP | | Declaration | location_of_goods_street_number | string | ✓ | ✓ | ✓ | 1 | | Declaration | delivery_terms | string | x | x | ✓ | DDP, DDU | | Declaration | delivery_terms_location | string | x | x | ✓ | NLAMS (UN/LOCODE) | | Declaration | total_invoice_value | decimal | x | x | ✓ | 12.34 | | Declaration | currency_code | string | x | x | ✓ |EUR | | Declaration | documents | []object | ✓ | ✓ | ✓ | N380, N740 | | Item | additional_procedure_codes | []string | ["F48", "C07"] | ["F49", "C07"] | x | ["F48", "C07"] | | Item | national_additional_codes | []string | x | x | ✓ | ["Y160"] | ### location_of_goods 这个根据各个国家要求不同会相对复杂,所以支持两种写法: ```json { "location_of_goods": { "country_code": "NL", "postal_code": "1118 CP", "street_number": "1" } } ``` ```json { "location_of_goods_country_code": "NL", "location_of_goods_postal_code": "1118 CP", "location_of_goods_street_number": "1" } ``` ## Servers - production: https://app.gutline.eu/api/v1 (production) - sandbox: https://app-sandbox.gutline.eu/api/v1 (sandbox) ## Authentication ## Endpoints and operations ### [Declarations](https://docs.gutline.eu/group/endpoint-declarations.md) - [获取申报列表](https://docs.gutline.eu/operation/operation-getdeclarations.md) - [创建申报](https://docs.gutline.eu/operation/operation-createdeclaration.md) - [获取申报详情](https://docs.gutline.eu/operation/operation-getdeclaration.md) - [取消申报](https://docs.gutline.eu/operation/operation-canceldeclaration.md) - [确认申报](https://docs.gutline.eu/operation/operation-presentdeclaration.md) ### [Waybills](https://docs.gutline.eu/group/endpoint-waybills.md) - [按提单批量申报](https://docs.gutline.eu/operation/operation-createwaybill.md) [Powered by Bump.sh](https://bump.sh)