Error format
{
"error": "error_code",
"message": "Human-readable message",
"field": "risk.insuredAmount",
"requestId": "550e8400-e29b-41d4-a716-446655440000"
}
Responses use a stable machine error code and a human-readable message. Optional field names the JSON path when validation fails.
Request ID
Every HTTP response sets header X-Request-Id. Error JSON (HTTP 4xx/5xx, and any body that includes error — including 200 duplicate_external_reference on apply) also includes requestId with the same value.
- You may send
X-Request-Id on the request; otherwise the server generates one.
- Idempotent replays return the current request's id, not the id from the cached response.
- Include
requestId in logs and support tickets so Gooval can locate the call in server logs.
Coverage limits
When insuredAmount is below the minimum or above the quotable cap, the API returns coverage_out_of_range with limit objects in integer minor units:
{
"error": "coverage_out_of_range",
"message": "insuredAmount is outside allowed coverage",
"field": "risk.insuredAmount",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"minCoverage": { "amount": 1000, "currencyCode": "USD" },
"maxCoverage": { "amount": 500000, "currencyCode": "USD" },
"hardMaxAmount": { "amount": 1000000, "currencyCode": "USD" }
}
| HTTP | error | Description |
| 401 | missing_auth_headers | Missing auth headers |
| 401 | invalid_signature | HMAC mismatch |
| 401 | timestamp_expired | Timestamp expired (outside ±5 minutes) |
| 429 | rate_limit_exceeded | Too many requests |
| 404 | policy_not_found | Unknown policyNo |
| 400 | field_required | Missing schema field |
| 400 | line_items_total_mismatch | Sum of lineItems amounts does not equal insuredAmount |
| 400 | invalid_url | imageUrl is not a valid https URL |
| 400 | invalid_email | buyer.email is missing @ / domain, or otherwise invalid |
| 400 | invalid_datetime | orderPlacedAt or estimatedArrivalDate is not a valid timestamp/date |
| 400 | order_too_old | orderPlacedAt is more than 24 hours before apply time |
| 400 | order_in_future | orderPlacedAt is after apply time |
| 400 | invalid_attachment_key | attachmentKey is missing or belongs to another store |
| 400 | too_many_attachments | More than 10 attachments |
| 400 | invalid_content_type | Upload file type is not pdf/jpeg/png/webp |
| 400 | invalid_content_length | Upload size is missing or exceeds 5 MB |
| 400 | currency_mismatch | Amount currency does not match shop settlement currency |
| 400 | shop_currency_not_configured | Open tenant missing shopCurrencyCode |
| 400 | country_not_supported | Destination country is not in the assigned protection catalog |
| 400 | coverage_out_of_range | insuredAmount is below min or above max coverage. Body also includes minCoverage, maxCoverage (quoted cap), and hardMaxAmount — each { amount, currencyCode } in minor units |
| 400 | premium_exceeds_max | insuredAmount × country rate exceeds the highest price tier |
| 400 | invalid_enum | Unknown reference-data code |
| 400 | invalid_merchant_shop | merchantShop is not an object |
| 400 | invalid_metadata | metadata is not a flat string map, a key is invalid, or a value exceeds 256 characters |
| 400 | too_many_metadata | More than 20 metadata keys |
| 400 | invalid_document_source | source is policy / insurer / reinsurer, or another unsupported value — Open API only issues Gooval confirmation PDFs |
| 400 | invalid_document_lang | lang is not zh or en |
| 409 | cancel_blocked_by_shipment | A tracking number is already present (including multi-shipment policies); whole-policy cancel is not allowed |
| 409 | single_tracking_policy | Policy already has a single risk.trackingNumber; syncShipments is not allowed |
| 400 | use_sync_shipments | Policy uses multi-shipment syncShipments; changeShipment is not allowed |
| 409 | cancel_blocked_by_claim | Policy has a completed claim or an incomplete claim application; cancel is not allowed |
| 200 | duplicate_external_reference | externalReference already has an active policy (includes requestId like other errors) |
| 502 | policy_apply_failed | Insurer rejected apply (mapped) |
Policies
POST
/open/v1/policies/quote
Postman: Quote
Calculate premium from insured amount; does not create a policy. Same pricing as apply: destination-country catalog rate + markup, matched to a price tier.
Quote does not require lineItems, orderPlacedAt, estimatedArrivalDate, buyer, or trackingNumber. It does not enforce the 24-hour order window.
merchantShop and metadata may be included but are ignored until apply. All amount fields use integer minor units — see Money.
Request body
| Field | Type | Required | Description |
externalReference | string | Yes | Merchant order ID |
risk.insuredAmount | object | Yes | { amount, currencyCode } — amount in minor units (cents) |
risk.destination | object | Yes | Destination address |
risk.departure | object | Yes | Origin address |
risk.trackingNumber | string | No | Tracking number (optional at quote) |
risk.carrierCode | string | No | 17track carrier code |
risk.carrierName | string | No | Carrier display name |
Example request
{
"externalReference": "ORD-1001",
"risk": {
"trackingNumber": "1Z999AA10123456784",
"carrierCode": "17",
"carrierName": "UPS",
"insuredAmount": { "amount": 18000, "currencyCode": "USD" },
"destination": {
"countryCode": "US", "city": "Los Angeles",
"provinceCode": "CA", "addressLine": "123 Main St"
},
"departure": {
"countryCode": "CN", "city": "Shenzhen",
"addressLine": "Factory Zone A"
}
}
}
200 response
{
"externalReference": "ORD-1001",
"insuredAmount": { "amount": 18000, "currencyCode": "USD" },
"premiumAmount": { "amount": 299, "currencyCode": "USD" },
"quoteExpiresInSeconds": 60
}
POST
/open/v1/policies/apply
Postman: Apply
Bind a policy on the Gooval platform after shipment. Returns policyNo immediately. Reinsurance (if the catalog product is not self-retained) is submitted asynchronously and is never exposed to merchants.
Requires Idempotency-Key header. Monetary amount fields use integer minor units — see Money.
Key fields
| Field | Required | Description |
externalReference | Yes | Your order ID |
orderPlacedAt | Yes | ISO-8601 order time, e.g. 2026-08-20T09:15:00Z. Must be within the past 24 hours (now − 24h through now). Older orders return 400 order_too_old; future timestamps return 400 order_in_future. |
risk.insuredAmount | Yes | Insured amount in minor units (cents) |
risk.destination / risk.departure | Yes | Address objects |
risk.trackingNumber | No | Optional until shipment. Single shipment: add or change with changeShipment (cannot cancel once set). Multiple shipments: omit at apply and use syncShipments endorse — see Multiple tracking numbers. |
risk.estimatedArrivalDate | No | YYYY-MM-DD. Omit at apply and supply later with changeShipment or per shipment in syncShipments. If omitted when reinsurance is submitted, Gooval defaults to sail date + 14 days. |
risk.lineItems | Yes | Merchandise breakdown (see below) |
buyer.name / buyer.email | Yes | Buyer contact; confirmation email contains spNumber |
attachments | No | Invoice / packing list / product photos (upload first) |
merchantShop | No | Platform-internal shop identity for settlement split (id ≤ 64, name ≤ 200). Gooval still bills the Open tenant as a whole; not sent to the reinsurer. |
metadata | No | Flat string map (max 20 keys; keys [A-Za-z0-9._-], ≤ 40 chars; values ≤ 256 chars) for extra settlement tags. |
Omitted merchantShop / metadata fields are not returned later. Endorsement and cancel do not change them.
Example request
{
"externalReference": "ORD-1002",
"orderPlacedAt": "2026-08-20T09:15:00Z",
"merchantShop": { "id": "shop_8821", "name": "Acme US" },
"metadata": { "channel": "tiktok", "settlementCode": "NA-01" },
"risk": {
"trackingNumber": "1Z999AA10123456784",
"carrierCode": "17",
"carrierName": "UPS",
"insuredAmount": { "amount": 10000, "currencyCode": "USD" },
"estimatedArrivalDate": "2026-08-25",
"destination": {
"countryCode": "US", "city": "Los Angeles",
"provinceCode": "CA", "addressLine": "123 Main St, Los Angeles, CA"
},
"departure": {
"countryCode": "CN", "city": "Shenzhen",
"addressLine": "Factory Zone A"
},
"lineItems": [{
"itemId": "LINE-001", "sku": "TEE-BLU-M", "name": "Blue Tee",
"category": "apparel",
"quantity": 2,
"unitPrice": { "amount": 5000, "currencyCode": "USD" },
"imageUrl": "https://cdn.merchant.com/tee.jpg"
}]
},
"buyer": { "name": "Jane Doe", "email": "buyer@example.com" }
}
lineItems rules (required)
- Each line requires
name, category (from reference-data/product-categories), quantity (≥1), unitPrice, and at least one of sku or itemId
- Sum of
unitPrice × quantity must equal risk.insuredAmount (same currency, minor units)
- Optional
imageUrl must be https. Lines are returned in claim lookup as orderLineItems
Omitting lineItems returns 400 field_required. Missing or unknown category returns field_required / invalid_enum. Legacy policies bound before this change still fall back to a single virtual line at claim lookup.
If trackingNumber is omitted at apply, reinsurance waits until a later changeShipment or syncShipments supplies it. Apply does not take a shipment list.
If externalReference already has an active policy, apply returns 200 with error: duplicate_external_reference — use GET /policies?externalReference= or POST .../cancel before re-applying.
200 success (merchant-visible)
{
"policyNo": "P2026070612345678",
"externalReference": "ORD-1002",
"status": "active",
"premium": { "amount": 299, "currencyCode": "USD" },
"insuredAmount": { "amount": 10000, "currencyCode": "USD" },
"merchantShop": { "id": "shop_8821", "name": "Acme US" },
"metadata": { "channel": "tiktok", "settlementCode": "NA-01" }
}
spNumber is never returned to merchants.
200 duplicate apply
Same shape as other errors: includes requestId (and response header X-Request-Id). Use GET /policies?externalReference= to fetch the existing policy, or cancel before re-applying.
{
"error": "duplicate_external_reference",
"message": "externalReference already has an active policy",
"field": "externalReference",
"requestId": "550e8400-e29b-41d4-a716-446655440000"
}
POST
/open/v1/policies/attachments/upload-url
Postman: Attachment upload URL
Get a presigned URL to upload policy attachments before apply.
POST /open/v1/policies/attachments/upload-url with { "contentType", "contentLength", "type"?, "fileName"? }
PUT the file to the returned uploadUrl (include the same Content-Type)
- Pass
attachmentKey on apply
Allowed types: invoice, packingList, productPhoto, other. Files: PDF / JPEG / PNG / WebP, 1 byte–5 MB, max 10 per apply. This route is not 24h-idempotent (presigned URLs expire in 900 seconds).
"attachments": [
{
"attachmentKey": "open-policy-attachments/{storeNumber}/{uuid}.pdf",
"type": "invoice",
"fileName": "invoice.pdf"
}
]
GET /policies/{policyNo} echoes orderPlacedAt and attachments (keys only; no download URL). Attachments are not sent to the reinsurer.
GET
/open/v1/policies
Postman: List policies
Query parameters
| Parameter | Default | Description |
cursor | — | Previous page nextCursor |
limit | 50 | 1–200 |
policyNo | — | Filter by policy number |
externalReference | — | Filter by order ID |
createdFrom / createdTo | — | ISO 8601 time range |
200 response
{
"items": [{
"policyNo": "P2026070612345678",
"externalReference": "ORD-1002",
"status": "active",
"orderName": "#1002",
"premium": { "amount": 299, "currencyCode": "USD" },
"createdAt": "2026-07-06T06:00:00+00:00"
}],
"nextCursor": "eyJ..."
}
GET
/open/v1/policies/{policyNo}
Get a single policy including orderPlacedAt, risk.shipments when using multi-shipment endorse (tracking number, carrier, line items, insured amount per shipment), attachment keys (no download URLs), and merchantShop / metadata when set at apply. Reinsurer policy numbers are not included.
GET
/open/v1/policies/{policyNo}/document
Get a pre-signed URL for the Gooval application confirmation PDF (documentType: confirmation). This is not an insurance policy.
The PDF is rendered from Gooval's HTML confirmation template (configured by Gooval Admin; en and zh layouts). Until a custom template is saved, the built-in default layout is used. Template updates change the PDF cache key — the next download regenerates the file.
Use ?lang=zh or ?lang=en (locale / language also accepted; default en). Reinsurer policy PDFs are not available on the Open API (source=policy returns 400 invalid_document_source).
{ "downloadUrl": "https://...", "expiresIn": 900, "documentType": "confirmation" }
POST
/open/v1/policies/{policyNo}/endorse
Postman: Endorse
Endorse a policy. Gooval applies the change immediately. If the policy is already submitted to a reinsurer, Ping An intelligentEndorse (edrSceneCode: ["900013"]) runs asynchronously and does not fail this request.
Request body
| Field | Required | Description |
endorseScene | Yes | Supported scenes from reference-data/endorse-scenes (see table below) |
changes | Yes | Fields to change |
reason / remark | No | Notes |
endorseScene values
| Code | Mutable fields |
changeDestination | destination, claimPaymentAddress, destinationContinentCategory |
changeShipment | trackingNumber, carrierCode, carrierName, sailDepartureDate, estimatedArrivalDate (single-tracking policies only; after syncShipments use use_sync_shipments) |
syncShipments | Full changes.shipments snapshot (1–10) for multi-shipment policies — see Multiple tracking numbers |
Example request
{
"endorseScene": "changeShipment",
"changes": {
"trackingNumber": "1Z999AA10987654321",
"carrierCode": "17",
"carrierName": "UPS",
"estimatedArrivalDate": "2026-08-28"
},
"reason": "Carrier re-issued tracking label",
"remark": "Original label voided"
}
POST
/open/v1/policies/{policyNo}/endorse
endorseScene: syncShipments
Register or update multiple tracking numbers on one platform policy. Apply does not accept a shipment list — omit risk.trackingNumber at apply when the order has not shipped, then call endorse with endorseScene: syncShipments as shipments go out.
Rules
- Each call sends the complete shipment list (1–10 shipments), not only new tracking numbers. A tracking number omitted from a later call is treated as removed.
- Each shipment requires
trackingNumber, carrierCode or carrierName, and lineItems (same line rules as apply; shipment insured amount in minor units).
- Optional per shipment:
estimatedArrivalDate, sailDepartureDate.
- One platform
policyNo. Reinsurance is submitted once per tracking number and is not exposed on this API. Unchanged tracking numbers are not resubmitted; new numbers are applied at the reinsurer; removed numbers that were already submitted are cancelled at the reinsurer while the platform policy stays active.
- Policies that already have a single
risk.trackingNumber cannot use syncShipments (409 single_tracking_policy). After the first syncShipments, changeShipment returns 400 use_sync_shipments.
Example request
{
"endorseScene": "syncShipments",
"changes": {
"shipments": [
{
"trackingNumber": "1Z999AA10123456784",
"carrierCode": "17",
"carrierName": "UPS",
"insuredAmount": { "amount": 9000, "currencyCode": "USD" },
"estimatedArrivalDate": "2026-08-25",
"lineItems": [{
"itemId": "LINE-001",
"sku": "TEE-BLU-M",
"name": "Blue Tee",
"category": "apparel",
"quantity": 1,
"unitPrice": { "amount": 9000, "currencyCode": "USD" }
}]
},
{
"trackingNumber": "1Z999AA10987654321",
"carrierCode": "17",
"insuredAmount": { "amount": 9000, "currencyCode": "USD" },
"lineItems": [{
"itemId": "LINE-002",
"name": "Red Tee",
"category": "apparel",
"quantity": 1,
"unitPrice": { "amount": 9000, "currencyCode": "USD" }
}]
}
]
},
"reason": "Order split into two packages"
}
GET /policies/{policyNo} echoes risk.shipments (tracking number, carrier, line items, insured amount). Reinsurer policy numbers are not included.
Whole-policy cancel (POST .../cancel) is only allowed before any tracking number exists. Removing one shipment in syncShipments is not a policy cancel. See Cancel.
POST
/open/v1/policies/{policyNo}/cancel
Postman: Cancel
Cancel a policy. Gooval cancels immediately only when the policy has no tracking number. If the policy is already submitted to a reinsurer, Ping An intelligentEndorse (edrSceneCode: ["00006"]) runs asynchronously and does not fail this request.
Request body
| Field | Required | Description |
cancelReasonCode | Yes | From reference-data/cancel-reasons |
cancelReason | No | Reason text |
remark | No | Remark |
{
"cancelReasonCode": "merchantRefund",
"cancelReason": "Order refunded before shipment",
"remark": "Customer cancelled order ORD-1001"
}
200 response status is cancelled.
Returns 409 cancel_blocked_by_shipment when any tracking number is present (including one shipment on a multi-shipment policy). Whole-policy cancel is only allowed before any tracking number exists. Removing a shipment via syncShipments is not a policy cancel.
Returns 409 cancel_blocked_by_claim when the policy has a completed claim or an incomplete claim application.
Postman mapping
| Request name | Method | Path |
| List policies (paginated) | GET | /open/v1/policies |
| List collections | GET | /open/v1/reference-data |
| Get reference data | GET | /open/v1/reference-data/claim-types |
| Cancel reasons | GET | /open/v1/reference-data/cancel-reasons |
| Quote | POST | /open/v1/policies/quote |
| Attachment upload URL | POST | /open/v1/policies/attachments/upload-url |
| Apply | POST | /open/v1/policies/apply |
| Get document | GET | /open/v1/policies/{policyNo}/document |
| Endorse | POST | /open/v1/policies/{policyNo}/endorse |
| Cancel | POST | /open/v1/policies/{policyNo}/cancel |
Environment example
{
"baseUrl": "https://sp-prod.gooval.io",
"OPEN_API_STORE_NUMBER": "100001",
"OPEN_API_SECRET": "<your-secret>"
}