{"openapi":"3.1.0","info":{"title":"POST /api/v1/credit/shopify-pull","version":"1.0.0","description":"Pull a merchant's Shopify underwriting data with a salted ephemeral token"},"servers":[{"url":"https://api.ondayzero.com","description":"Production"}],"paths":{"/api/v1/credit/shopify-pull":{"post":{"tags":["credit"],"summary":"Pull a merchant's Shopify underwriting data with a salted ephemeral token","description":"For partners that hold the merchant's Shopify Admin API token in their own vault and pass it encrypted (`PBKDF2-HMAC-SHA256` with a per-request random `salt` + `AES-256-GCM` bound to `shop_domain` + salted SHA-256 `token_hash`). DayZero decrypts the token only in memory, queries Shopify for orders, daily sales and refund summaries, disputes, and Shopify Capital balance transactions onto the merchant's DayZero business, and discards the plaintext token. Only the irreversible salted SHA-256 fingerprint (`sha256:{salt}:{hash}`) is stored in the database.","operationId":"pull_credit_merchant_shopify","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-business-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Business-Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShopifyPullRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope_ShopifyPullResponse_"}}}},"400":{"description":"Bad Request - Invalid input","content":{"application/json":{"example":{"error":"validation_error","message":"Validation failed: due_date: Input should be a valid date","errors":{"due_date":"Input should be a valid date"},"code":"GEN_002","request_id":"3f0e7c2a-9b4d-4e1a-8c6f-2d5b7a1e9c30"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized - Missing or expired token, an MCP OAuth token used against the REST API, or no access to this business","content":{"application/json":{"example":{"error":"unauthorized","message":"Authentication required","code":"AUTH_001","request_id":"3f0e7c2a-9b4d-4e1a-8c6f-2d5b7a1e9c30"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden - Insufficient permissions","content":{"application/json":{"example":{"error":"forbidden","message":"You don't have permission to access this resource","code":"AUTH_002","request_id":"3f0e7c2a-9b4d-4e1a-8c6f-2d5b7a1e9c30"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found - Resource does not exist","content":{"application/json":{"example":{"error":"not_found","message":"Resource not found","code":"NOT_FOUND_001","request_id":"3f0e7c2a-9b4d-4e1a-8c6f-2d5b7a1e9c30"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"validation_error","message":"Validation failed: amount: Input should be a valid integer","errors":{"amount":"Input should be a valid integer"},"code":"GEN_002","request_id":"3f0e7c2a-9b4d-4e1a-8c6f-2d5b7a1e9c30"}}}}},"x-fastapi-operation-id":"pull_credit_merchant_shopify_api_v1_credit_shopify_pull_post"}}},"components":{"schemas":{"ShopifyPullRequest":{"properties":{"program_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Program Id","description":"Your lending program id. Required without `credit_line_id`; with it, must match the line's program if sent."},"external_merchant_id":{"anyOf":[{"type":"string","maxLength":128},{"type":"null"}],"title":"External Merchant Id","description":"Your id for the merchant, as sent on `POST /credit/lines` and `POST /credit/bank-feeds`. Required without `credit_line_id`."},"credit_line_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Credit Line Id","description":"The merchant's open credit line id, when one already exists."},"merchant_legal_name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Merchant Legal Name","description":"Names the merchant's DayZero business when this call creates it. Ignored when the business already exists."},"merchant_business_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Merchant Business Id","description":"The merchant's existing DayZero business id, when already created by a bank feed or bank connection."},"shop_domain":{"type":"string","maxLength":255,"minLength":4,"title":"Shop Domain","description":"The merchant's Shopify store domain (`*.myshopify.com`)."},"encrypted_token":{"type":"string","maxLength":4096,"minLength":1,"title":"Encrypted Token","description":"Base64 AES-256-GCM ciphertext of the Shopify Admin API access token, encrypted with the per-request `salt` and `iv` and bound to the normalized `shop_domain` as additional authenticated data."},"salt":{"type":"string","maxLength":64,"minLength":32,"pattern":"^[0-9a-fA-F]{32,64}$","title":"Salt","description":"Per-request random hex salt (16–32 bytes) used for PBKDF2-HMAC-SHA256 key derivation and the salted `token_hash`."},"iv":{"type":"string","maxLength":32,"minLength":24,"pattern":"^[0-9a-fA-F]{24,32}$","title":"Iv","description":"Hex initialization vector (12–16 bytes) for AES-256-GCM."},"auth_tag":{"type":"string","maxLength":32,"minLength":32,"pattern":"^[0-9a-fA-F]{32}$","title":"Auth Tag","description":"16-byte hex authentication tag produced by AES-256-GCM."},"token_hash":{"type":"string","maxLength":64,"minLength":64,"pattern":"^[0-9a-fA-F]{64}$","title":"Token Hash","description":"Salted SHA-256 hex digest of `{salt}:{shop_domain}:{access_token}` verifying token integrity after transport decryption."}},"additionalProperties":false,"type":"object","required":["shop_domain","encrypted_token","salt","iv","auth_tag","token_hash"],"title":"ShopifyPullRequest","description":"Pull a merchant's Shopify underwriting data using a salted, encrypted ephemeral token.\n\nThe partner encrypts the Shopify Admin API token with a per-request random\nsalt and the shared transport secret (`PBKDF2-HMAC-SHA256` + `AES-256-GCM`)\nand attaches a salted SHA-256 `token_hash`. DayZero decrypts the token only\nin memory to query Shopify's API (orders, daily summaries, refunds,\ndisputes, and Shopify Capital balance transactions) and discards the\nplaintext token without ever saving it to the database."},"ShopifyPullResponse":{"properties":{"merchant_business_id":{"type":"string","title":"Merchant Business Id","description":"The merchant's DayZero business where the Shopify underwriting figures were stored."},"shop_domain":{"type":"string","title":"Shop Domain","description":"Normalized `*.myshopify.com` store domain."},"token_fingerprint":{"type":"string","title":"Token Fingerprint","description":"Irreversible salted SHA-256 fingerprint (`sha256:{salt}:{hash}`) recorded in the database; the Shopify access token is never stored."},"token_persisted":{"type":"boolean","title":"Token Persisted","description":"Always false: the Shopify token is decrypted only in memory to query Shopify and discarded immediately after the pull.","default":false},"orders_synced":{"type":"integer","title":"Orders Synced","description":"Number of Shopify orders pulled and upserted for underwriting.","default":0},"summary_days":{"type":"integer","title":"Summary Days","description":"Number of shop days with at least one order in the lookback window.","default":0},"refunds_synced":{"type":"integer","title":"Refunds Synced","description":"Number of refunds stored from the pulled orders.","default":0},"disputes_synced":{"type":"integer","title":"Disputes Synced","description":"Number of Shopify disputes pulled across the 400-day chargeback window.","default":0},"capital_transactions_synced":{"type":"integer","title":"Capital Transactions Synced","description":"Number of Shopify Capital balance transactions pulled across the 90-day window.","default":0},"orders_complete":{"type":"boolean","title":"Orders Complete","description":"Whether every order in the lookback window has been read, over this call and any earlier ones it carried on from. False when Shopify failed part-way (the next pull starts over) or orders are left to read (`orders_resume_pending`); the Shopify measures stay `shopify_data_stale`, and the underwriting readiness check waits on `shopify_sync`, until a pull completes.","default":false},"orders_resume_pending":{"type":"boolean","title":"Orders Resume Pending","description":"True when the store has more orders than one call reads: the position is kept, and the next call with the same envelope fields (a fresh salt and iv) carries on from it instead of starting over. Call again until `orders_complete` is true.","default":false},"disputes_complete":{"type":"boolean","title":"Disputes Complete","description":"Whether the disputes were read back over the whole window. False when the token lacks the disputes scope or Shopify failed; `shopify_chargeback_rate` then stays `dispute_history_incomplete`.","default":false},"synced_at":{"type":"string","format":"date-time","title":"Synced At","description":"When the ephemeral pull completed, UTC."}},"type":"object","required":["merchant_business_id","shop_domain","token_fingerprint","synced_at"],"title":"ShopifyPullResponse","description":"Summary of an ephemeral Shopify underwriting data pull."},"SuccessEnvelope_ShopifyPullResponse_":{"properties":{"success":{"type":"boolean","title":"Success","default":true},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code"},"data":{"anyOf":[{"$ref":"#/components/schemas/ShopifyPullResponse"},{"type":"null"}]}},"additionalProperties":true,"type":"object","title":"SuccessEnvelope[ShopifyPullResponse]"},"ErrorResponse":{"title":"ErrorResponse","type":"object","description":"Envelope returned by every non-2xx response. Branch on `code` (stable) rather than `message` (human-readable, may change).","required":["error","message","code"],"properties":{"error":{"type":"string","description":"Error category: `validation_error`, `unauthorized`, `forbidden`, `not_found`, `conflict`, `rate_limited`, `server_error`, or `service_unavailable`."},"message":{"type":"string","description":"Human-readable explanation, safe to show to end users."},"code":{"type":"string","description":"Stable machine-readable code in `CATEGORY_NNN` form (e.g. `NOT_FOUND_006`, `AUTH_010`, `GEN_002`)."},"request_id":{"type":"string","description":"Correlation id for support requests. Echoes the `x-request-id` request header when one was supplied."},"errors":{"type":"object","additionalProperties":{"type":"string"},"description":"Field-level validation messages keyed by field name. Present on 400/422 validation failures only."}}}},"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"API Token","description":"API token authentication. Format: `Bearer dz_...`"}}},"security":[{"BearerAuth":[]}]}