mkinyua53 / unhash
Reverse-lookup (unhash) phone numbers from SHA-256 hashes, with a local lookup table, bulk hash generation, and a configurable HTTP API fallback, for Laravel.
Requires
- php: ^8.2
- illuminate/console: ^11.0|^12.0
- illuminate/database: ^11.0|^12.0
- illuminate/http: ^11.0|^12.0
- illuminate/support: ^11.0|^12.0
Requires (Dev)
- orchestra/testbench: ^9.0|^10.0
- pestphp/pest: ^2.0|^3.0
README
Reverse-lookup ("unhash") phone numbers from SHA-256 hashes in Laravel, backed by a local lookup table with a configurable HTTP API fallback, plus a console command to bulk-generate the lookup table for a set of MSISDN prefixes.

Installation
composer require mkinyua53/unhash
The service provider and Unhash facade are registered automatically via Laravel package discovery.
Publish the config and migration if you want to customize them:
php artisan vendor:publish --tag=unhash-config
php artisan vendor:publish --tag=unhash-migrations
php artisan migrate
Configuration
See config/unhash.php. Key options (all overridable via .env):
| Env var | Purpose |
|---|---|
UNHASH_TABLE | Table name for the local hash lookup (default hashed_numbers) |
UNHASH_PREFIXES_TABLE | Table name for the MSISDN prefixes list (default unhash_prefixes) |
UNHASH_LOG_CHANNEL | Log channel used during bulk hash generation (default unhash) |
UNHASH_LOOKUP_LOG_CHANNEL | Log channel used during getNumber() lookups (default hashes) |
UNHASH_LOGGING_ENABLE | Enable/disable all package logging (default false) |
UNHASH_QUEUE_CONNECTION | Queue connection used for both hash-generation jobs (default: the application's default connection) |
UNHASH_PREFIX_JOB_QUEUE | Queue name for GenerateHashesForPrefixJob (default: the connection's default queue) |
UNHASH_BATCH_JOB_QUEUE | Queue name for GenerateHashesForPrefixBatchJob (default: the connection's default queue) |
UNHASH_API_ENABLED | Enable/disable the remote API fallback (default true) |
UNHASH_API_URL | Remote endpoint that decodes a hash into a phone number |
UNHASH_API_KEY | API key sent with the remote request |
UNHASH_API_KEY_HEADER | Header name for the API key (default x-api-key) |
The remote API is expected to accept POST {url} with JSON body {"hash": "..."} and respond with JSON containing a phone key.
Usage
use Mkinyua53\Unhash\Facades\Unhash;
$phoneNumber = Unhash::getNumber($hash);
Resolution order: local hashed_numbers table lookup, then the remote API fallback (if configured).
Managing prefixes
The set of MSISDN prefixes hashes are generated for lives in the unhash_prefixes database table, not in config/unhash.php. The prefixes array in the config file is only used to seed that table the first time its migration runs — after that, the table is the source of truth, so a consuming app can extend the list without publishing/editing the package config:
use Mkinyua53\Unhash\Facades\Unhash;
Unhash::prefixes(); // all prefixes currently tracked, e.g. ['0110', '0700', ...]
Unhash::addPrefix('0680'); // add one (must match /^0\d{3}$/); returns false if malformed or already present
Unhash::removePrefix('0680'); // remove one; returns whether a row was removed
php artisan unhash:generate-hashes (with no prefix argument) reads from this table, so newly added prefixes are picked up automatically.
Generating the lookup table
php artisan unhash:generate-hashes # all configured prefixes
php artisan unhash:generate-hashes 0722 # a single prefix
php artisan unhash:generate-hashes --delete-duplicates
php artisan unhash:generate-hashes --delete-all-duplicates
php artisan unhash:generate-hashes 0722 --delete-duplicates # scoped to a single prefix
Each prefix dispatches a Mkinyua53\Unhash\Jobs\GenerateHashesForPrefixJob queued job, which in turn dispatches one Mkinyua53\Unhash\Jobs\GenerateHashesForPrefixBatchJob per 10,000-number batch (100 per prefix by default). Both jobs are ShouldBeUnique — per prefix, and per prefix+batch respectively — so batches can be processed in parallel across queue workers and a failed/retried batch doesn't require re-scanning the whole prefix. Both jobs also check hash_prefix_counts first and skip immediately once a prefix has reached 1,000,000 generated hashes, so re-dispatching an already-complete prefix (or a stray retried batch) is cheap regardless of how large hashed_numbers has grown.
--delete-duplicates/--delete-all-duplicates scan for duplicate phone_number rows one prefix at a time (all prefixes in unhash_prefixes, or just the one given as an argument), skipping any prefix range with no rows at all, instead of scanning the whole hashed_numbers table in one pass. A prefix removed from unhash_prefixes after being generated won't be covered by this scan.
Which connection and queue each job runs on is configurable via config('unhash.queue') (UNHASH_QUEUE_CONNECTION, UNHASH_PREFIX_JOB_QUEUE, UNHASH_BATCH_JOB_QUEUE) — useful for routing the lighter-weight prefix-dispatch job and the heavier batch-processing job to different queues/workers.
Database support
The package is database-agnostic: index checks use Schema::hasIndex(), duplicate cleanup uses the query builder (subqueries, whereIn/whereNotIn), and bulk inserts use insertOrIgnore(). This has been tested against SQLite and MySQL and should also work on PostgreSQL. Requires Laravel 11+ for Schema::hasIndex()/schema introspection.
insertOrIgnore() is not supported on SQL Server — the unhash:generate-hashes command will not work there, though the lookup path (Unhash::getNumber()) is unaffected.
Testing
composer install
vendor/bin/pest