https://app.gutline.eu/api/v1
海关申报 API 提供申报管理功能,包括创建、确认和取消申报。
重要说明:申报一旦提交到海关系统就不能修改,如需修改请重新创建申报。
认证
参考 Bearer token 认证方式
幂等请求
API 支持幂等请求,您可以安全的重试请求,而不会意外的执行两次相同操作,要执行一个幂等请求须在请求头中增加:
Idempotency-Key: <key> 重试时保证 key 不变
幂等请求仅支持使用 POST 方式的请求,其他方式的请求将被忽略
分页
分页按照 rfc5988 标准
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
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... 是签名值。
请求体结构
{
"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 示例
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 示例
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,触发重试
// 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- 拒绝原因
处理示例:
# 检查是否被接受
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: 这种设计有两个重要优势:
- 避免存储压力:不需要存储大量过期数据
- 过期数据无意义:过期数据对业务没有价值
- 精确判断:时间戳字段(如
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
这个根据各个国家要求不同会相对复杂,所以支持两种写法:
{
"location_of_goods": {
"country_code": "NL",
"postal_code": "1118 CP",
"street_number": "1"
}
}
{
"location_of_goods_country_code": "NL",
"location_of_goods_postal_code": "1118 CP",
"location_of_goods_street_number": "1"
}
This is version 1.0.0 of this API documentation. Last update on Dec 5, 2025.