Skip to content

Running the platform

Admin and vendor panels

Sharkhex Delivery includes two browser-based administration areas:

  • Main admin panel.
  • Vendor admin panel.

Main admin panel

Login route:

/admin/login

The main admin panel is intended for platform operators.

Main sections include:

  • Dashboard.
  • Live ops.
  • Problems reported.
  • Users.
  • Contacts.
  • Courier applications.
  • Courier documents.
  • Account deletions.
  • Vendor types.
  • Vendors.
  • Categories.
  • Products.
  • Banners.
  • Offers.
  • Attachments.
  • Orders.
  • Bookings.
  • Parcels.
  • Jobs.
  • Routes.
  • Currencies.
  • Payments.
  • Payment providers.
  • Payment accounts.
  • Metadata definitions.
  • Metadata values.
  • Notifications.
  • Platform settings.
  • Payment settings.
  • Storage settings.
  • Notification settings.
  • Integration settings.
  • Courier onboarding settings.
  • Legal documents.
  • FAQ.
  • Customer credit.
  • Audit log.

Live ops

Operations → Live ops is the dispatcher's screen. It lists every order on its way, oldest first (or newest first), with how long it has been waiting overall and in its current status. One click filters the orders that need attention:

  • No courier: no courier has the delivery after Settings → Delivery & Dispatch → Live ops: flag an order with no courier (5 minutes by default).
  • Store hasn't accepted: the store hasn't accepted the order after the store threshold (5 minutes).
  • Stuck: the order has stayed in one status longer than the stuck threshold (45 minutes).
  • Scheduled: held until its delivery time; never flagged as late.

Below the orders are the open parcel jobs and a map of the couriers who are online (green: free; orange: on a job). The page refreshes every 30 seconds, except while you are filling in a form.

Open an order to act on it:

  • Assign / Reassign courier: pick an online courier in the order's zone (nearest first, with their open jobs), or send the job back to dispatch, which offers it to the nearest courier other than the previous one. Offers still waiting for an answer are withdrawn. The previous courier, the new courier, the customer and the store are all notified. A job can be reassigned until the courier sets off with it.
  • Move the order on: only to the next legal status. Delivered completes the delivery and pays the courier, as the app does; it is not offered for a cash order until the courier has confirmed the cash.
  • Alert again: sends the store its new-order alert, or the customer the order's status, again.
  • Cancel (or Cancel and refund once paid): needs a reason. The customer is refunded in full, and the store and courier are told.

The order page also shows its history (every action with who did it and what changed) and the chat between the customer and the courier.

Orders, jobs, bookings, parcels, routes and delivery events can be browsed in their own lists but no longer edited by hand: Manage on an order or job opens it in Live ops, so the courier, the store and the customer always stay in step.

Reports

System → Reports shows a period of the platform's days (this month by default; quick buttons for today, yesterday, the last 7 days, this month and last month). Sales count the orders that reached a store and weren't cancelled, by when they were placed: orders, product sales, what customers paid, commission, average basket, cancellations and problems reported, with a chart per day (per hour for a single day). Below: the money on those orders line by line, what every wallet moved in the period (the ledger, by type), and the period by store, by zone and by courier.

Download for accounting gives the period's orders (every amount of every order), payouts and cash collected by couriers as Excel or CSV. Files are built while they download, so a month of orders comes straight away.

Problems reported by customers

Within Settings → General → Customers can report a problem hours of delivery (48 by default), customers can report a problem from the order screen: missing item, wrong item, damaged or spilled, late, never arrived, or something else. They pick the items concerned, describe what happened and add up to 3 photos (stored privately; only admins can open them).

Operations → Problems reported lists the open reports, oldest first, and Live ops shows how many are waiting. Open one to see the items and their price, the photos and the order's history, then settle it:

  • Refund amount: pre-filled with the reported items' price. It goes back to the card the customer paid with (anything beyond what the card paid becomes credit), or to their credit if you choose. A cash order is always refunded as credit.
  • Refund everything: whatever has not been refunded yet.
  • Give as credit: compensation in credit, paid by the platform. No money moves on the order.
  • Reject: closes the report without compensation. Add a message explaining why.

Who pays a refund: for a missing, wrong or damaged item, the store, by default. The refund is taken from its earnings, up to the price of its products with tax; the platform keeps its commission. For lateness, a delivery that never arrived and anything else, the platform pays. You can change it for each refund.

The customer is told the outcome by push and email, with your message. The store sees the report and any amount taken from its earnings on the order in its app, and gets a push when it is charged. Every refund gets its own credit note.

Customer credit

Credit comes from compensation and from refunds paid as credit. Customers see their balance and its history under Profile → Credit. At checkout, "Use my credit" (on by default) pays as much of the order as the credit covers. A card payment is never left below the smallest amount Stripe can charge. If credit covers the whole order, nothing is charged to the card; for cash orders, the courier collects only what's left. The store earns the same either way: the platform pays the credit. Credit used by an order comes back if the order is cancelled, including when an unpaid checkout is abandoned (the scheduled job app:orders:expire-unpaid cancels those after Cancel orders left unpaid after minutes, 60 by default). Payments → Customer credit lists every balance.

The app's home screen shows, under the store types: banners, Order again (the customer's own recent stores), Featured, Popular near you (most delivered orders in the customer's zone over the last 30 days) and Open now. Only stores that deliver to the selected address appear.

  • Banners (Vendors & Catalog → Banners): a wide image (about 3:1), a title and what tapping it opens: a store, a store type, a promo code (shown to the customer to use at checkout) or a link. A banner shows while Published and between its From and Until times, in order of Position. Limit it to some zones or one language, or show it on store-type screens instead of home. The list shows which banners are showing now.
  • Featured: set Featured From (and optionally Featured Until) on a store.

Reviews and moderation

Customers rate the store and the courier after a delivered order (thumbs up or down, with an optional comment). A store's score is the share of thumbs up from its customers; the admin-entered Average Rating only shows until the store has ratings.

  • Stores see their ratings in the app (Profile → Reviews, and a tile on the home screen with the score and how many comments are waiting for an answer), and can publish one answer per review, change it or take it down. The customer gets a notification. Customers see recent comments and answers on the store page.
  • Reporting: customers and the store can report a review. It stays visible, marked as reported, until someone at the platform decides.
  • Moderation: Vendors & Catalog → Ratings & Reviews lists reported reviews first, with the reason. Hide removes a review from the app and from the store's or courier's score; Publish brings it back. Reviews are hidden rather than deleted.

Help screen FAQ

Settings → FAQ holds the questions and answers on the app's Help screens, per language and per audience: Shown to customers, couriers or stores, each on their own Help screen. People whose language has none see the platform's default language. With no entries for an audience, its FAQ row is hidden.

Audit log

System → Audit log records who did what and when:

  • every create, edit and delete in the admin and vendor panels, with each changed field before and after;
  • every Live ops action, cancellation and refund (with its reason), payout marked paid or cancelled, and cash remittance;
  • every other admin button (approve, verify, run now, ...).

Passwords, tokens, keys, bank account numbers and secret settings are masked. Entries keep the person's email, so they still read correctly after an account is deleted. Filter by action, person, record type, record id or date.

Vendor admin panel

Login route:

/vendor-admin/login

The vendor admin panel is intended for vendor users.

Vendor sections include:

  • Dashboard.
  • Business details.
  • Products.
  • Offers.
  • Orders.

Vendor users must be connected to a vendor record before they can access vendor data. If no vendor is linked to the account, the user receives a friendly blocked-access screen.

Store reports

Sales → Reports in the store panel (and Reports in the store app) shows the store's sales for any period of its own days: sales, orders, average basket, cancellations and problems, a chart per day (per hour for a single day) and the best sellers. People who can see payouts also see their money, which is what their balance moved in the period (earnings after commission, tips, refunds and cancellations), so it always matches the ledger, and can download the period's orders (with commission, free delivery paid, tips, refunds and net per order) and ledger lines for their accountant, as Excel or CSV. Managers see the sales without the money.

Sold out items, stock and menu hours

Each product has an Availability:

  • Available.
  • Sold out today: back by itself when the store next opens (or at midnight for a store without opening hours). Nobody has to switch it back on.
  • Sold out until a chosen time.
  • Unavailable: sold out until the store changes it.

Sold-out items stay on the menu, marked as sold out, and can't be ordered. To hide an item instead, switch off Active.

  • Stock (optional): units left. Each order takes from it when it's placed, and a cancelled order puts its units back. At 0 the item shows as sold out by itself. Leave it empty to not count.
  • Menu hours (optional): when the item is on the menu, in the store's time zone (breakfast until 11:00, for example). Outside them the item is left off the menu and refused at checkout. Empty means whenever the store is open.

Stores change these in the vendor admin panel (Products) or in the app: Sold out today / Back in stock on the product list, and a Sold out switch on each option in the product's options. If something in a customer's cart sells out before they pay, checkout names it and asks them to remove it.

Allergens, dietary tags and calories

Each product can list the allergens it contains (the 14 EU food law requires: celery, gluten, crustaceans, eggs, fish, lupin, milk, molluscs, mustard, nuts, peanuts, sesame, soy, sulphites), dietary tags (vegetarian, vegan, halal, kosher, gluten-free, lactose-free, spicy) and its calories. Stores set them in the app's product form or the vendor admin panel, and admins on the product form. Customers see the tags on the menu and the product page, with "Contains: …" for the allergens, and can filter the store list by vegetarian, vegan, gluten-free or halal (stores with at least one such dish on the menu).

In the vendor admin panel, Catalog → Import menu adds or updates up to 2,000 products at once from a spreadsheet saved as CSV (comma, semicolon or tab separated). The store app links to it from Products; admins can use it for any store by opening the vendor admin panel for that store.

  1. Download the template. Its columns are category, name, description, price, allergens, tags, availability and calories. Only name and price are required; allergens and tags are separated by semicolons.
  2. Upload the filled-in file. Every row is checked and the preview shows how many products are new, how many will be updated, and each row with a problem (by row number, with the reason).
  3. Import brings in the rows that passed; rows with problems are skipped. A row with the same name as one of the store's products updates it; nothing is deleted. The import runs through Messenger, so with an async transport it happens in the background and the page shows its progress.

Categories must already exist (the page lists them); importing a product into a category also adds that category to the store's menu sections.

Store promotions

Stores run their own promotions from the app (Profile → Promotions) or the vendor admin panel (Offers):

  • Percent off, or an amount off each item, on the whole menu, one category or one product;
  • Free delivery on the whole store;
  • with a start and an optional end, and, for whole-store promotions, an optional minimum order.

Customers see the discounted prices on the menu, the product sheet and in the cart, and a store's running promotions as tags on the store page and its cards. An item gets the single best offer: offers don't add up. A minimum order is checked against the basket at full price, so a menu discount above a minimum shows at checkout once the basket reaches it; the cart tells the customer how much more they need for free delivery.

The store pays for its own promotions: item discounts lower its sales (and the commission on them), and with free delivery it pays the delivery fee instead of the customer, out of its earning on the order. Admins can also create offers for any store, or platform offers on a product or category, under Offers.

Store settings

In the app, Profile → Store settings lets a store:

  • choose how it takes new orders: accept each one, or accept automatically. An automatic store accepts each order as soon as it reaches the store (once paid, once a cash order is confirmed, or when a scheduled order is released), with its preparation time. The customer is told, and a courier is offered as usual. Admins can set it on the store too (New Orders);
  • set its usual preparation time, used for the delivery estimate;
  • turn the new-order sound on or off on that device;
  • set a minimum order;
  • set its own delivery fee, only when the platform allows it (Settings → Delivery → Stores can set their own delivery fee in the app);
  • edit its opening hours in its time zone: several ranges a day, closed days, or open around the clock.

Chains and store roles

A store's account owner (the person it was created for, who is also paid for it) is its owner. Other people get a role at a store or at a whole chain:

  • Owner: everything, including the store's money and its team.
  • Manager: runs the store (orders, menu, promotions, settings, reviews and figures) but not payouts or the team.
  • Staff: takes orders, opens or pauses the store and marks items sold out. No revenue, settings, menu editing, promotions or payouts.

Store owners add managers and staff in the app (Profile → Team): an email address and a role. People without an account get an email to set a password, valid for 7 days; they then sign in to the app or this panel with it. Owners can change a role or remove someone.

Chains: under Vendors & Catalog → Chains create a chain, then pick it on each store's form (Chain). Under Store roles, give someone a role at the chain to cover every store in it. A chain's owners and managers see its combined figures on the app's home screen, and anyone with menu rights at two stores can copy a menu from one to the other (Products → Copy the menu from another store; products with the same name are updated).

Someone with several stores switches between them from the app's home screen, or under Your stores in the vendor panel's menu. New-order notifications go to everyone at the store who handles orders.

Courier onboarding

Any app user can apply to deliver from Profile → Become a courier: vehicle, plate (motorised vehicles), date of birth, city, and photos or PDFs of their documents. Which documents are required is set in Settings → Courier onboarding: an identity document, and for motorbikes, cars and vans a driving licence, vehicle registration and insurance; a work permit is off by default. Documents that expire need an expiry date.

Only an approved courier can go online, receive delivery offers or take parcels. Review applications in Courier applications: open one to view each document, then:

  • Approve: accepts the documents still waiting for review and lets the courier start delivering.
  • Reject: sends the application back with your reason; the courier fixes it in the app and sends it again. Rejecting a single document in its row does the same for that document.
  • Suspend: takes an approved courier offline with a reason. Reinstate needs every required document accepted and in date.

The daily app:kyc:check-expiry job warns couriers before a document expires and suspends couriers whose required document has expired; they upload the new one in the app and you reinstate them once it is accepted. Couriers are told about every decision by push and email.

Courier vehicles

An approved courier changes vehicle in the app (Profile → Vehicle). Bicycles, e-bikes and e-scooters switch straight away; a motorbike, car or van needs its driving licence, registration and insurance accepted first (they upload them under Courier documents, and you accept them in Courier applications).

The vehicle decides what each courier is offered:

  • Distance: Settings → Delivery has a longest delivery per vehicle type (e.g. 3 km for bicycles; 0 = no limit, the default). Longer jobs go to other couriers.
  • Parcels: Operations → Parcel sizes and Parcel weights list the vehicles that can carry each class (none ticked = any). A parcel job goes only to couriers whose vehicle every one of its parcels allows, on the board, in offers and when taking a job.
  • Estimates: while a store has no delivery history of its own, its delivery estimate is refined with the accepting courier's vehicle speed.

The same rules apply when you reassign a job in Live ops. Couriers without a vehicle on file have no distance limit but aren't offered vehicle-restricted parcels.

Courier shifts

Couriers book hourly shifts in the app (Profile → Shifts). You open them with shift patterns in Operations → Courier shifts: a zone (or all zones), the days, the hours and how many couriers per hour. Times are in the platform's time zone; patterns for the same zone and hour add their places up. Saving a pattern opens its slots straight away, and the daily app:shifts:generate job opens each new day, as far ahead as Settings → Delivery → Courier shifts: days ahead couriers can book. Changing or removing a pattern only touches slots nobody has booked.

Whether shifts matter is set in Settings → Delivery → Courier shifts: who gets offers:

  • free (the default): any online courier is offered jobs, as before shifts; booking is optional.
  • slots_preferred: couriers on a booked shift covering the job's zone are offered it first, then the others, nearest first in each group.
  • slots_required: only couriers on a booked shift are offered jobs, see the parcel board or can take a parcel.

Couriers can cancel up to the cutoff before a shift starts. Every 5 minutes app:shifts:tick reminds them by push before a run of booked hours starts, checks in those who are online once their hour has begun (going online during a booked hour checks them in at once), and marks those still offline after the grace period as no-shows. Courier applications shows each courier's Missed shifts (sortable and filterable). Operations → Shift coverage shows each zone's week by hour: couriers booked against places, checked in (✓) and no-shows (✗), with names on hover.

Courier earnings and cash

Couriers see their earnings in the app (Profile → Earnings, or Activity) by day, week or month: delivery pay, surge bonus, tips and any adjustments, every delivery with its pay, and their payouts with status and period. The figures are read straight from their wallet ledger (Wallets), so they always match it. Cash taken on cash-on-delivery orders is shown separately with how much they hold now; it's deducted from their next payout. Put how couriers hand cash in (e.g. office address and hours) in Settings → Payments → How couriers hand in the cash they collect; it's shown on the courier's cash card. When they do, use Wallets → Mark cash remitted.

Failed deliveries

A courier who can't finish a delivery must say why: customer not answering, wrong address, customer refused, store closed, store can't hand it over, vehicle problem, or something else (with a note). They can add a photo.

  • Customer not answering: only after they have called or messaged the customer (calls from the app and messages in the order chat both count) and waited Settings → Delivery → Failed deliveries: a courier can mark the customer unreachable… minutes (10 by default).
  • Vehicle problem before pickup: the job goes back to dispatch for another courier and the order carries on.
  • Any other failure: the order becomes delivery failed, the customer and the store are notified, and an issue opens in Operations → Problems reported for the refund decision, with the courier's note and photo. The suggested liability is the store when it was closed or couldn't hand the order over, otherwise the platform. A customer who couldn't be reached is normally not refunded.

Each store chooses what happens to an order the courier couldn't deliver after collecting it (Vendors → Undeliverable Orders, or Store settings in the store app): the courier disposes of it, or brings it back and confirms the return. The courier is paid per Failed deliveries: courier pay… (full, base pay only, or nothing; by default full when it failed at the customer's door and base pay when it failed at the store). A courier's own vehicle problem pays nothing; tips are only paid on delivered orders. Live ops shows the reason, the note and the courier's tries to reach the customer. Parcel collection and delivery failures need a reason too.

Phone sign-in

People sign in with their phone number: the app sends a six-digit code by text message and the first sign-in with a new number creates a customer account (they then give their name, and an email for receipts if they like). Email and password sign-in keeps working, and a phone number added and verified under Personal info signs in too. Admin and store web panels stay on email.

Texts go through Twilio or Vonage, set in Settings → Notifications: Text messages (sign-in codes): provider, then that provider's credentials and sender. Until a provider is set, an account that has an email gets its phone code by email instead, and new numbers can't sign up by phone. Codes expire after 10 minutes; one can be requested per number per minute and 10 per network per hour; five wrong codes lock the number for 15 minutes. Only numbers from Phone sign-in: countries… receive codes (empty = the platform's country), which keeps paid texts from being abused. Users shows each person's sign-in phone.

Sign-in security

Settings → Sign-in security can ask for a code on top of the password when someone signs in from a device they haven't verified before: off (default), staff (admins, stores and couriers) or everyone. The code goes to the account's email, or by text when it has none. It applies to the app, the website, this panel and the store panel; phone sign-in doesn't need it. Ticking Don't ask again on this device remembers the device for Remember a verified device for days (30; 0 asks at every sign-in).

A user's page lists their verified devices; Forget verified devices makes all of them enter a code again. Setting a new password here, or a password reset by the user, also signs every app session out and forgets the devices.

Delivery codes are protected the same way: five wrong codes on one order lock it for 15 minutes, so a courier can't guess the customer's code. The door photo of an order left at the door is kept private: it shows on the order in Live Ops and on the job's page, for admins only.

Referrals and membership

Invite friends. Turn on Settings → Referrals & Membership → Invite friends and set the credit for both sides. Each customer gets a code in the app (Profile → Invite friends). A new customer enters a friend's code before their first order; they need a verified phone number. When that first order is delivered, both get the credit, once. A phone number or device can only be referred once, and a code stops working after the set number of friends. Payments → Referrals lists them.

Membership. Turn on Membership, name it and set the monthly price, the basket from which delivery is free, and the service fee discount. Customers join under Profile with a saved card; Stripe charges them monthly. Members pay no delivery fee from that basket and a lower service fee; the platform covers both, so stores and couriers are paid as usual. Cancelling in the app keeps the benefits until the paid month ends. Add customer.subscription.created, customer.subscription.updated and customer.subscription.deleted to the Stripe webhook so renewals, failed payments and cancellations reach the platform. Payments → Memberships lists them; refunds are made in the Stripe dashboard.

Partner API and webhooks

A store with its own till or ordering system (POS), or an integrator working for a chain, connects through the partner API. Under Partner API → API keys, create a key for one store or a whole chain and tick what it may do: Orders (see, accept, reject, mark ready, change the preparation time) and Menu (push products, prices and availability). The key is shown once, right after it's made, so copy it then; only a fingerprint of it is kept. Revoke stops it on its next call, and Last used shows when it was last seen.

Under Partner API → Webhooks, add the address where the store's or chain's events go and tick the events (none ticked: all of them). Every event is signed with the webhook's secret, shown on its page; New secret replaces it, and the partner must then switch to the new one. Send test event sends a ping and shows the answer. Webhook deliveries lists every event sent with its answer and tries. An address that's down is retried after 1, 5, 30, 120 and 360 minutes (the app:webhooks:retry job, every minute); Retry now resends a failed one. The partner's developers find the API reference at /api/doc/partner.

Vendor onboarding

Use Vendors → Invite vendor to create a store and its manager account. The store is hidden from customers; the manager gets an email with a link (valid for 7 days, single use) to set a password. If the email already has an account, the store is linked to it instead. Resend invite sends a fresh link.

In the vendor panel the manager fills in Business details (legal name, tax ID, registration number and registered address, printed on receipts) and sends the store for review; those fields lock until you decide. In Vendors, Approve makes the store live, Reject sends it back with a reason, Suspend hides a live store and Reinstate brings it back. Customers only ever see stores that are approved and active, and only those can take orders.

Linking a user to a vendor in the admin gives that user the vendor role automatically. Vendors created directly with Add vendor start approved.

Account deletion

People delete their account in the app (Settings → Delete account, after re-entering their password) or on the web at /account/delete (an emailed code proves they own the address). The account is signed out everywhere at once and anonymised 30 days later by the daily app:account:process-deletions job; signing in again before then cancels it.

Deletion is refused while there is an order, parcel or delivery in progress, while a courier is online, while money is owed either way (earnings not yet paid out, or cash collected on deliveries), while a payout is being processed, or while the person manages a store. The app and the web page say which.

Anonymising wipes the person's name, email, phone, addresses, saved cards (the Stripe customer), bank details, courier documents, tokens, cart, favourites and ratings. Orders, receipts, invoices and wallet ledgers stay, without the personal data, because tax law requires them.

Account deletions lists every request with its source and status, and what is holding back a due one. Erase now skips the grace period; Keep account cancels the deletion. Deleting a user from Users goes through the same anonymisation instead of deleting their orders and wallet.

Terms of service, privacy policy, courier terms and store terms are served at /legal/terms, /legal/privacy, /legal/rider-terms and /legal/vendor-terms (add ?lang=xx for a language). Until you publish your own, built-in English templates filled with your legal details are shown: have them reviewed before launch.

In Legal documents, Create drafts from templates gives editable copies in your default language. Edit a draft, then Publish. Published versions are never edited: New version copies one into a draft, and publishing it archives the old one. Sign-up records the terms version each customer accepted. To host a document elsewhere instead, set the terms or privacy URL in Settings → General.

Admin access to vendor admin

Administrators can access the vendor admin area. This is useful when support staff need to inspect or assist with vendor data.

From the admin vendor list, use the vendor admin action to open the vendor admin panel scoped to a selected vendor.

The vendor context can also be selected through a vendor identifier in the URL where supported:

/vendor-admin?vendorId=123

Role requirements

Admin panel:

ROLE_ADMIN

Vendor admin panel:

ROLE_VENDOR

or:

ROLE_ADMIN

Access denied handling

The admin and vendor admin login flows are designed to show a friendly access message when the authenticated user does not have the right role, instead of exposing a raw Symfony exception page.

If a user sees an access denied message:

  • Confirm the account has the correct role.
  • Confirm the user account is active.
  • For vendor admin access, confirm the user is linked to a vendor.
  • For admin support access to vendor admin, open the vendor admin from the Vendors section.

Creating the first admin

Use the console command:

php bin/console app:admin:create-user admin@example.com strong-password

After login, review platform settings before inviting vendors or testing payments.

Found a mistake? Tell us.