Open API

Gooval REST API for self-hosted merchants. Base path: /open/v1

Overview

ItemDescription
Base URLhttps://{api-host}/open/v1
Content-Typeapplication/json
EncodingUTF-8
Field namingcamelCase
TimestampsISO 8601 UTC

Environments

EnvironmentBase URL
Productionhttps://sp-prod.gooval.io/open/v1
Developmenthttps://sp-test.gooval.io/open/v1

Set Postman variable {{baseUrl}} to the host root (without /open/v1), e.g. https://sp-prod.gooval.io.

Authentication

Every request must include HMAC signature headers. Never call the Open API from a browser or frontend — keep the API secret on your backend only.

Auth header names remain X-Gwofy-* (legacy naming, matches the current Gooval backend — send headers exactly as listed below).

Credentials

Before integrating, obtain storeNumber (store ID) and apiSecret (API secret). Contact Gooval to enable access:

After onboarding, Gooval provides storeNumber and apiSecret (shown once — store immediately on your server; never commit to git or expose in the client).

CredentialUsagePostman / Apifox variable
storeNumberHeader X-Gwofy-Store-NumberOPEN_API_STORE_NUMBER
apiSecretHMAC signing (not sent in headers)OPEN_API_SECRET

Headers

HeaderRequiredDescription
X-Gwofy-Store-NumberYesTenant store number
X-Gwofy-TimestampYesUnix timestamp (seconds), ±5 minutes
X-Gwofy-SignatureYesHMAC-SHA256 hex signature
Content-TypeYesapplication/json
Idempotency-KeyWrite opsIdempotency key; same key within 24h returns the same response

Signature

message = storeNumber + "\n" + timestamp + "\n" + rawBody
signature = hex(hmac_sha256(apiSecret, message))
  • rawBody: raw HTTP request body string; empty string "" for GET
  • Signature is lowercase hexadecimal

Environment variables

VariableDescription
OPEN_API_STORE_NUMBERStore number
OPEN_API_SECRETAPI secret (server-side only)

Rate limits

Default per-store limits (60-second sliding window):

BucketRoutesDefault limit
quotesPOST /policies/quote6000/min
policies_writeapply / endorse / cancel30/min
attachmentsPOST /policies/attachments/upload-url120/min
policies_readGET policies, events, document120/min
reference_dataGET reference-data300/min

When exceeded, returns 429 with Retry-After header. Cache quote results server-side (30–60s) for checkout traffic.

Error format

{
  "error": "error_code",
  "message": "Human-readable message",
  "field": "risk.insuredAmount"
}

Responses use a stable machine error code and a human-readable message. Optional field names the JSON path when validation fails.

HTTPerrorDescription
401missing_auth_headersMissing auth headers
401invalid_signatureHMAC mismatch
401timestamp_expiredTimestamp expired (outside ±5 minutes)
429rate_limit_exceededToo many requests
404policy_not_foundUnknown policyNo
400field_requiredMissing schema field
400line_items_total_mismatchSum of lineItems amounts does not equal insuredAmount
400invalid_urlimageUrl is not a valid https URL
400invalid_emailbuyer.email is missing @ / domain, or otherwise invalid
400invalid_datetimeorderPlacedAt or estimatedArrivalDate is not a valid timestamp/date
400order_too_oldorderPlacedAt is more than 24 hours before apply time
400order_in_futureorderPlacedAt is after apply time
400invalid_attachment_keyattachmentKey is missing or belongs to another store
400too_many_attachmentsMore than 10 attachments
400invalid_content_typeUpload file type is not pdf/jpeg/png/webp
400invalid_content_lengthUpload size is missing or exceeds 5 MB
400currency_mismatchAmount currency does not match shop settlement currency
400shop_currency_not_configuredOpen tenant missing shopCurrencyCode
400country_not_supportedDestination country is not in the assigned protection catalog
400coverage_out_of_rangeinsuredAmount is below min or above max coverage
400premium_exceeds_maxinsuredAmount × country rate exceeds the highest price tier
400invalid_enumUnknown reference-data code
400invalid_merchant_shopmerchantShop is not an object
400invalid_metadatametadata is not a flat string map, a key is invalid, or a value exceeds 256 characters
400too_many_metadataMore than 20 metadata keys
400invalid_document_sourcesource is policy / insurer / reinsurer, or another unsupported value — Open API only issues Gooval confirmation PDFs
400invalid_document_langlang is not zh or en
409cancel_blocked_by_shipmentPolicy already has a tracking number; cancel is not allowed
409cancel_blocked_by_claimPolicy has a completed claim or an incomplete claim application; cancel is not allowed
200duplicate_external_referenceexternalReference already has an active policy
502policy_apply_failedInsurer rejected apply (mapped)

Core flow

1. GET /reference-data/{collection} → load enums (countries, carriers, product-categories, endorse-scenes, …) 2. POST /policies/quote → checkout premium (optional; cache 30–60s server-side) 3. POST /policies/apply → bind policy on the Gooval platform (does not call external reinsurers synchronously) 4. Receive policyNo + externalReference (not spNumber) 5. Gooval emails buyer with spNumber for /claim/* lookup (new: {storeNumber}-{14 random digits}; legacy {storeNumber}-{externalReference} still valid) 6. Subscribe to outbound webhooks (Portal) or poll GET /events

Premium uses the same formula as the Shopify storefront: insured amount × destination-country rate (catalog rate + merchant markup), matched to the cheapest protection price tier at or above that fee. Quote uses the same pricing as apply.

On quote and apply, risk.insuredAmount.currencyCode (and optional risk.lineItems[].unitPrice) must match the tenant's shopCurrencyCode (single settlement currency per store).

Reference data

GET /open/v1/reference-data Postman: List collections

List all reference-data collection names.

200 response

{
  "schemaVersion": "2026.07.1",
  "collections": [
    "countries", "currencies", "carriers", "product-categories",
    "destination-continent-categories", "goods-value-categories",
    "certificate-types", "endorse-scenes", "cancel-reasons",
    "claim-types", "policy-statuses", "claim-customer-statuses"
  ]
}
GET /open/v1/reference-data/{collection} Postman: Get reference data

Get enum items for a collection. Supports If-None-Match; returns 304 when unchanged.

Path parameters

ParameterDescription
collectione.g. product-categories, cancel-reasons, endorse-scenes, claim-types

200 example — cancel-reasons

{
  "schemaVersion": "2026.07.1",
  "collection": "cancel-reasons",
  "items": [
    { "code": "merchantRefund", "name": "Merchant refund before shipment" },
    { "code": "duplicatePolicy", "name": "Duplicate policy" },
    { "code": "customerWithdraw", "name": "Customer withdrawal" },
    { "code": "other", "name": "Other" }
  ]
}

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.

Request body

FieldTypeRequiredDescription
externalReferencestringYesMerchant order ID
risk.insuredAmountobjectYes{ amount, currencyCode }
risk.destinationobjectYesDestination address
risk.departureobjectYesOrigin address
risk.trackingNumberstringNoTracking number (optional at quote)
risk.carrierCodestringNo17track carrier code
risk.carrierNamestringNoCarrier display name

Example request

{
  "externalReference": "ORD-1001",
  "risk": {
    "trackingNumber": "1Z999AA10123456784",
    "carrierCode": "17",
    "carrierName": "UPS",
    "insuredAmount": { "amount": "180.00", "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": "180.00", "currencyCode": "USD" },
  "premiumAmount": { "amount": "2.99", "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.

Key fields

FieldRequiredDescription
externalReferenceYesYour order ID
orderPlacedAtYesISO-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.insuredAmountYesInsured amount
risk.destination / risk.departureYesAddress objects
risk.trackingNumberNoOptional until shipment. Add or change later with changeShipment. A policy that already has a tracking number cannot be cancelled.
risk.estimatedArrivalDateNoYYYY-MM-DD. Omit at apply and supply later with changeShipment. If omitted when reinsurance is submitted, Gooval defaults to sail date + 14 days.
risk.lineItemsYesMerchandise breakdown (see below)
buyer.name / buyer.emailYesBuyer contact; confirmation email contains spNumber
attachmentsNoInvoice / packing list / product photos (upload first)
merchantShopNoPlatform-internal shop identity for settlement split (id ≤ 64, name ≤ 200). Gooval still bills the Open tenant as a whole; not sent to the reinsurer.
metadataNoFlat 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": "180.00", "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": "50.00", "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, ±0.01 rounding)
  • 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 supplies it. GET .../document returns a Gooval application confirmation (not a reinsurer policy).

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": "2.99", "currencyCode": "USD" },
  "insuredAmount": { "amount": "180.00", "currencyCode": "USD" },
  "merchantShop": { "id": "shop_8821", "name": "Acme US" },
  "metadata": { "channel": "tiktok", "settlementCode": "NA-01" }
}

spNumber is never returned to merchants.

200 duplicate apply

{
  "error": "duplicate_external_reference",
  "message": "externalReference already has an active policy",
  "field": "externalReference"
}
POST /open/v1/policies/attachments/upload-url Postman: Attachment upload URL

Get a presigned URL to upload policy attachments before apply.

  1. POST /open/v1/policies/attachments/upload-url with { "contentType", "contentLength", "type"?, "fileName"? }
  2. PUT the file to the returned uploadUrl (include the same Content-Type)
  3. 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

ParameterDefaultDescription
cursor—Previous page nextCursor
limit501–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": "2.99", "currencyCode": "USD" },
    "createdAt": "2026-07-06T06:00:00+00:00"
  }],
  "nextCursor": "eyJ..."
}
GET /open/v1/policies/{policyNo}

Get a single policy including orderPlacedAt, attachment keys (no download URLs), and merchantShop / metadata when set at apply.

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.

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

FieldRequiredDescription
endorseSceneYesSupported scenes from reference-data/endorse-scenes (see table below)
changesYesFields to change
reason / remarkNoNotes

endorseScene values

CodeMutable fields
changeDestinationdestination, claimPaymentAddress, destinationContinentCategory
changeShipmenttrackingNumber, carrierCode, carrierName, sailDepartureDate, estimatedArrivalDate

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}/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

FieldRequiredDescription
cancelReasonCodeYesFrom reference-data/cancel-reasons
cancelReasonNoReason text
remarkNoRemark
{
  "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 risk.trackingNumber is present, and 409 cancel_blocked_by_claim when the policy has a completed claim or an incomplete claim application.

Events (polling)

GET /open/v1/events

Query parameters

ParameterDescription
sinceISO 8601; events after this time only
type / eventFilter by event type
limitDefault 50

Event types: policy.insured · policy.failed · policy.endorsed · policy.cancelled · claim.submitted · claim.status_changed · claim.completed

Outbound webhooks

Configure in the Merchant portal (/portal/webhook-config), or contact contact@gooval.io.

Gooval POSTs JSON events to your URL. Payloads include policyNo and externalReference — never spNumber. When set at apply, they also include merchantShop and metadata.

Webhook signature header remains X-Gwofy-Signature (legacy naming, matches backend).

Subscribed events

policy.insured · policy.failed · policy.endorsed · policy.cancelled · claim.submitted · claim.status_changed · claim.completed

Verification

X-Gwofy-Signature = hex(hmac_sha256(webhook_secret, timestamp + "." + rawBody))

Postman mapping

Request nameMethodPath
List policies (paginated)GET/open/v1/policies
List collectionsGET/open/v1/reference-data
Get reference dataGET/open/v1/reference-data/claim-types
Cancel reasonsGET/open/v1/reference-data/cancel-reasons
QuotePOST/open/v1/policies/quote
Attachment upload URLPOST/open/v1/policies/attachments/upload-url
ApplyPOST/open/v1/policies/apply
Get documentGET/open/v1/policies/{policyNo}/document
EndorsePOST/open/v1/policies/{policyNo}/endorse
CancelPOST/open/v1/policies/{policyNo}/cancel

Environment example

{
  "baseUrl": "https://sp-prod.gooval.io",
  "OPEN_API_STORE_NUMBER": "100001",
  "OPEN_API_SECRET": "<your-secret>"
}