Fulfillment tracks how an order is delivered after payment. A fulfillment covers a set of the order's line items and has a type: shipment, pickup, local_delivery, digital, or service. Create one with POST /v1/orders/{order_id}/fulfillments.
Create a fulfillment with one package#
Send shipment.packaging: single_package to put the full quantities of the submitted line_items into one outbound shipment and one package. This choice does not select other items on the order. Omit shipment when you want to add shipments and packages separately. Shipment input is accepted only with type: shipment.
The paid order must have unallocated fulfillment quantities, and each item's fulfillment snapshot must allow the requested type. Shipping items use quoted delivery requirements. Checkout may already have created their fulfillment; read the order first and add shipments and packages to that existing fulfillment when present. If you create a physical fulfillment before checkout materializes one, your integration owns fulfillment execution for the order, including any quantities left after a partial create.
POST /v1/orders/ord_01J00000000000000000000000/fulfillments
Authorization: Bearer flint_test_...
Idempotency-Key: warehouse-order-1042-box-1
Content-Type: application/json
{
"type": "shipment",
"line_items": [{"order_line_item_id": "li_01J00000000000000000000000", "quantity": 2}],
"shipment": {
"packaging": "single_package",
"package": {
"carrier": "ups",
"service_code": "ground",
"tracking_number": "1Z999AA10123456784",
"buyer_notification_behavior": "suppress"
}
}
}
The nested package accepts the same fields as package creation: carrier, service_code, tracking_number, tracking_url, label_url, external_system, external_reference_id, weight, dimensions, buyer_notification_behavior, status_reason, and metadata. A non-Flint label_url requires package external_system. Shipment provenance belongs on shipment.external_system and shipment.external_reference_id; it does not supply package provenance.
Tracking notifications are requested by default. Set package buyer_notification_behavior: suppress before submitting when this creation should not email the buyer. The package starts at created, even with tracking. Use mark_shipped or a carrier event after handoff.
The response returns the created fulfillment inside data. When you include shipment input, its shipments and packages fields contain the created shipment and package. Read the package items with GET /v1/packages/{package_id}/items. Without shipment input, shipments and packages are absent. Subsequent changes use the existing shipment, package, and package-item routes. Package items remain independent children, and concurrent allocations cannot exceed the fulfillment's quantities.
The fulfillment, shipment, package, item allocations, events, and notification intents commit together. A failure commits none of them. Buyer messages and webhooks are delivered asynchronously. Shipment progress, fulfillment completion, and payment status remain separate.
Idempotency-Key is optional. Supply a key you retain before sending when you need to recover a lost response: replay the same key and body to retrieve the original IDs without another package or notification intent. Do not start a new create with a new key while the original outcome is unknown. If you supplied fulfillment external_reference_id, list fulfillments using order_id and that reference to reconcile accepted work, then read its shipments, packages, events, and notifications. A generated key returned only in a lost response cannot be reconstructed.
Track fulfillment progress#
status tracks public work progress. request_status separately records whether the provider accepted the request. An accepted request has request_status: accepted and status: pending until work starts. While active_hold is present, status remains at the public work status from before the hold. mark_no_show returns status: failed and records the reason in outcome. The status filter on GET /v1/fulfillments uses these same projected values.
Read supported_actions before changing a fulfillment. Send one of those values as action to POST /v1/fulfillments/{fulfillment_id}/transitions. The action values are accept, schedule, hold, start, mark_preparing, mark_picked, mark_packed, mark_ready, dispatch, fail, mark_no_show, complete, and cancel. The available subset depends on the fulfillment type and current status.
curl -X POST https://api.withflintpay.com/v1/fulfillments/ful_01J.../transitions \
-H "Authorization: Bearer $FLINT_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: fulfillment-ful_01J...-ready" \
-d '{
"action": "mark_ready",
"expected_version": 4
}'
Each action accepts only its documented fields. schedule requires scheduled_start_at and scheduled_end_at. hold and mark_no_show require a reason from their action-specific enum. fail requires release_quantity: use true to release the fulfillment's quantity or false to preserve it. Every action accepts expected_version; when supplied, it must match the fulfillment's version. A stale version or an action outside supported_actions returns 409 with the current fulfillment, including its current version, status, and supported_actions.
Transition responses include the updated fulfillment, the status-change event, and any buyer notification audit records. Buyer notifications are sent by default. Set buyer_notification_behavior to suppress when the transition should not notify the buyer.
PATCH /v1/fulfillments/{fulfillment_id} also accepts optional expected_version. Use it when changing mutable fields such as the recipient, location, metadata, or external reference.
For a pickup that checkout creates, pickup_details names the store the buyer collects from: location_name, address, and instructions, which are the pickup instructions of the delivery method the buyer chose. A note the buyer left for the store is in recipient.instructions.
For a shipment or local delivery, recipient.address is the address where that fulfillment will run. When a create request omits recipient, Flint copies the order's delivery_destination and uses the order buyer email. Supplying recipient overrides that default for the new fulfillment. Later recipient changes apply only to that fulfillment.
Physical shipping has three levels below the order. A fulfillment holds one or more shipments, each shipment holds packages, and each package holds items that allocate order line-item quantities to that box. Create and manage item allocations under /v1/packages/{package_id}/items. A shipment can split one fulfillment across multiple parcels without treating package items as unrelated top-level resources.
Carrier tracking belongs to packages. Create a shipment to group one carrier leg, then create one package per physical parcel. Put carrier, service_code, tracking_number, tracking_url, dimensions, weight, and label information on each package, including single-parcel shipments. A shipment can remain empty only as a created draft. Its read-only status and movement timestamps are derived from its package states. Package exceptions dominate the aggregate; otherwise the least advanced active package determines shipment progress. PATCH /v1/shipments/{shipment_id} and POST /v1/shipments/{shipment_id}/void accept optional expected_version; a stale value returns 409 with the current shipment and version.
Fulfillment transitions accept a free-text reason_message for your note. The hold and mark_no_show actions instead require an enum reason. Shipment and package void requests also accept reason_message.
Packages publish their own supported_actions. Send one of those values to POST /v1/packages/{package_id}/transitions. Package actions are mark_packed, mark_shipped, mark_in_transit, mark_out_for_delivery, mark_delivered, mark_delivery_attempted, mark_exception, and mark_returned. Each package transition accepts optional reason_message, occurred_at, buyer_notification_behavior, and expected_version. PATCH /v1/packages/{package_id} and POST /v1/packages/{package_id}/void also accept optional expected_version. Voiding a package remains a separate operation.
A shipment also has a direction. Outbound is the default and carries goods to the buyer. A return shipment carries them back: set direction to return, then provide return_id and return_line_items, each allocating a whole-number quantity against a return_line_item_id on that Return. The two shapes do not mix. Direction is fixed at creation. List shipments by return_id to find the inbound leg for a return.
Send carrier and 3PL facts to POST /v1/fulfillments/{fulfillment_id}/events. Set shipment_id or package_id when the fact applies to a nested resource. Events accept observational types such as shipped, in_transit, out_for_delivery, delivered, delivery_attempted, tracking_updated, exception, returned, and custom. Use external_event_id for idempotent carrier ingestion, with external_system, external_status, occurred_at, and location details for provenance.
GET /v1/fulfillment-events is the pollable record of the same fulfillment event stream delivered by webhooks. Filter it by order_id, fulfillment_id, shipment_id, package_id, or event_type. Status-change events include previous_status, current_status, quantity_effect, and created_at. Caller notes appear in reason_message; hold and mark_no_show codes appear in the enum reason.
GET /v1/fulfillment-notifications is the buyer-email audit. It records whether each fulfillment email is pending, sent, failed, or suppressed. It is not another status timeline.
A package transition such as mark_delivered records carrier progress. It does not complete the fulfillment. Use the fulfillment's complete action when the merchant accepts that the obligation is discharged.
Order-level fulfillment_status distinguishes not_applicable for orders with nothing to fulfill and closed for obligations resolved through a mix of completed and canceled quantities.
Fulfillment also consumes inventory. Stock committed when an order is paid stays physically on hand until the handoff event for that fulfillment type. Canceling before handoff releases the claim without moving stock, and a refund alone never restocks anything. Returned units re-enter stock through Return operations.
Configure delivery methods and quote buyer choices in Delivery configuration.
Fulfillment webhooks omit recipient. Read fulfillment.resource_url from the webhook and fetch the fulfillment when your OMS or 3PL needs the address.
Terminal transition time#
Fulfillment reads and lifecycle webhook snapshots include terminal_at after the fulfillment enters a terminal state: completed, canceled, or failed. It records the terminal state transition and remains unchanged by later edits. completed_at is separate completion evidence, such as the delivery time, and can differ from terminal_at.
